Compare commits
26 Commits
55bf0ebea5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 78c51d55b5 | |||
| 514f1cb483 | |||
| 77edd1b666 | |||
| fb1c28a0ba | |||
| c39b8085af | |||
| eb43b35873 | |||
| f57bdd68da | |||
| e4b3689e64 | |||
| 84355f2463 | |||
| e4259978de | |||
| 9a10ff4727 | |||
| 4dc64c22cb | |||
| ebaeb1722d | |||
| 07a8276899 | |||
| bc1d5a2796 | |||
| baa43de4e1 | |||
| c693cde038 | |||
| 9583b7030c | |||
| cf5d988bbc | |||
| 0b6c6736ef | |||
| e8be239c32 | |||
| 3684d9ef6b | |||
| d38001e3e2 | |||
| 02dfc1568c | |||
| 9441291070 | |||
| 04a61b71ef |
36
.claude/settings.local.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebFetch(domain:nova.laravel.com)",
|
||||||
|
"Bash(test:*)",
|
||||||
|
"mcp__context7__resolve-library-id",
|
||||||
|
"mcp__context7__query-docs",
|
||||||
|
"Bash(herd php:*)",
|
||||||
|
"Bash(composer:*)",
|
||||||
|
"Bash(rsync:*)",
|
||||||
|
"mcp__playwright__browser_navigate",
|
||||||
|
"mcp__playwright__browser_fill_form",
|
||||||
|
"mcp__playwright__browser_click",
|
||||||
|
"mcp__playwright__browser_handle_dialog",
|
||||||
|
"mcp__playwright__browser_snapshot",
|
||||||
|
"mcp__playwright__browser_close",
|
||||||
|
"WebFetch(domain:docs.laravel-excel.com)",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(herd composer require:*)",
|
||||||
|
"Bash(npm run build:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(xargs:*)",
|
||||||
|
"mcp__playwright__browser_take_screenshot",
|
||||||
|
"Write",
|
||||||
|
"Bash",
|
||||||
|
"mcp__playwright__browser_console_messages",
|
||||||
|
"mcp__playwright__browser_navigate_back",
|
||||||
|
"mcp__playwright__browser_run_code",
|
||||||
|
"mcp__playwright__browser_wait_for",
|
||||||
|
"WebFetch(domain:www.bakertilly.nl)",
|
||||||
|
"mcp__playwright__browser_type",
|
||||||
|
"mcp__playwright__browser_hover",
|
||||||
|
"mcp__playwright__browser_evaluate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[compose.yaml]
|
||||||
|
indent_size = 4
|
||||||
71
.env.example
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
APP_NAME="Go No Go"
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://go-no-go.test
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=go-no-go
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AZURE_CLIENT_ID=
|
||||||
|
AZURE_CLIENT_SECRET=
|
||||||
|
AZURE_REDIRECT_URI=/auth/callback
|
||||||
|
AZURE_TENANT_ID=common
|
||||||
|
NOVA_LICENSE_KEY=
|
||||||
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
BIN
.playwright-mcp/step-01-homepage.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/step-03-login-jonathan.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/step-03-nova-login.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
.playwright-mcp/step-04-layout.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
.playwright-mcp/step-05-landing.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
.playwright-mcp/step-05-screening-result.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
.playwright-mcp/step-05-screening.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
.playwright-mcp/step-05-session-result.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
.playwright-mcp/step-05-session-show.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
277
.playwright-mcp/step10-score-check.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
- generic [ref=e3]:
|
||||||
|
- banner [ref=e4]:
|
||||||
|
- generic [ref=e6]: Piccadilly
|
||||||
|
- main [ref=e7]:
|
||||||
|
- generic [ref=e140]:
|
||||||
|
- generic [ref=e141]:
|
||||||
|
- heading "Audit Questionnaire" [level=1] [ref=e142]
|
||||||
|
- generic [ref=e417]:
|
||||||
|
- generic [ref=e418]:
|
||||||
|
- generic [ref=e419]: "5"
|
||||||
|
- generic [ref=e420]: points
|
||||||
|
- generic [ref=e421]: Consult Leadership
|
||||||
|
- generic [ref=e143]:
|
||||||
|
- heading "Basic Information" [level=2] [ref=e144]
|
||||||
|
- paragraph [ref=e145]: All fields are required before you can proceed to the questionnaire.
|
||||||
|
- generic [ref=e146]:
|
||||||
|
- generic [ref=e147]:
|
||||||
|
- generic [ref=e148]: Client Name
|
||||||
|
- textbox "Client Name" [ref=e149]:
|
||||||
|
- /placeholder: Enter client name
|
||||||
|
- generic [ref=e150]:
|
||||||
|
- generic [ref=e151]: Client Contact
|
||||||
|
- textbox "Client Contact" [ref=e152]:
|
||||||
|
- /placeholder: Enter client contact
|
||||||
|
- generic [ref=e153]:
|
||||||
|
- generic [ref=e154]: Lead Firm Name
|
||||||
|
- textbox "Lead Firm Name" [ref=e155]:
|
||||||
|
- /placeholder: Enter lead firm name
|
||||||
|
- generic [ref=e156]:
|
||||||
|
- generic [ref=e157]: Lead Firm Contact
|
||||||
|
- textbox "Lead Firm Contact" [ref=e158]:
|
||||||
|
- /placeholder: Enter lead firm contact
|
||||||
|
- button "Save Basic Info" [ref=e160]
|
||||||
|
- generic [ref=e161]:
|
||||||
|
- heading "Opportunity Details" [level=2] [ref=e162]
|
||||||
|
- generic [ref=e163]:
|
||||||
|
- generic [ref=e164]:
|
||||||
|
- paragraph [ref=e165]: What sort of audit opportunity is it?
|
||||||
|
- textbox "Enter your response..." [ref=e167]
|
||||||
|
- generic [ref=e168]:
|
||||||
|
- paragraph [ref=e169]: How many locations involved in this opportunity?
|
||||||
|
- textbox "Enter your response..." [ref=e171]
|
||||||
|
- generic [ref=e172]:
|
||||||
|
- paragraph [ref=e173]: List any locations included in this opportunity where we do not have a Baker Tilly firm.
|
||||||
|
- textbox "Enter your response..." [ref=e175]
|
||||||
|
- generic [ref=e176]:
|
||||||
|
- paragraph [ref=e177]: Where is the client HQ?
|
||||||
|
- textbox "Enter your response..." [ref=e179]
|
||||||
|
- generic [ref=e180]:
|
||||||
|
- paragraph [ref=e181]: Who is the competition?
|
||||||
|
- textbox "Enter your response..." [ref=e183]
|
||||||
|
- generic [ref=e184]:
|
||||||
|
- heading "Client Background and History" [level=2] [ref=e185]
|
||||||
|
- paragraph [ref=e186]: If you answer yes, you will score 1 point, if you answer no you will score 0 points
|
||||||
|
- generic [ref=e187]:
|
||||||
|
- generic [ref=e188]:
|
||||||
|
- paragraph [ref=e189]: What is the client's business and industry?
|
||||||
|
- textbox "Enter your response..." [ref=e191]
|
||||||
|
- generic [ref=e192]:
|
||||||
|
- paragraph [ref=e193]: There have been no significant changes in the client's business operations or structure recently?
|
||||||
|
- generic [ref=e195]:
|
||||||
|
- generic [ref=e196] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e197]
|
||||||
|
- generic [ref=e198]: "Yes"
|
||||||
|
- generic [ref=e199] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e200]
|
||||||
|
- generic [ref=e201]: "No"
|
||||||
|
- generic [ref=e202] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e203]
|
||||||
|
- generic [ref=e204]: N/A
|
||||||
|
- generic [ref=e205]:
|
||||||
|
- paragraph [ref=e206]: Does the sector and/or client come with a reputation which we are comfortable that Baker Tilly is associated with?
|
||||||
|
- generic [ref=e208]:
|
||||||
|
- generic [ref=e209] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e210]
|
||||||
|
- generic [ref=e211]: "Yes"
|
||||||
|
- generic [ref=e212] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e213]
|
||||||
|
- generic [ref=e214]: "No"
|
||||||
|
- generic [ref=e215] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e216]
|
||||||
|
- generic [ref=e217]: N/A
|
||||||
|
- generic [ref=e218]:
|
||||||
|
- paragraph [ref=e219]: Are there any previous audit reports or findings that need to be considered?
|
||||||
|
- generic [ref=e220]:
|
||||||
|
- generic [ref=e221]:
|
||||||
|
- generic [ref=e222] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e223]
|
||||||
|
- generic [ref=e224]: "Yes"
|
||||||
|
- generic [ref=e225] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e226]
|
||||||
|
- generic [ref=e227]: "No"
|
||||||
|
- generic [ref=e228] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e229]
|
||||||
|
- generic [ref=e230]: N/A
|
||||||
|
- generic [ref=e422]:
|
||||||
|
- generic [ref=e423]: Details (required)
|
||||||
|
- textbox "Enter details..." [ref=e424]
|
||||||
|
- generic [ref=e231]:
|
||||||
|
- heading "Financial Information" [level=2] [ref=e232]
|
||||||
|
- paragraph [ref=e233]: If you answer yes, you will score 1 point, if you answer no you will score 0 points
|
||||||
|
- generic [ref=e234]:
|
||||||
|
- generic [ref=e235]:
|
||||||
|
- paragraph [ref=e236]: Has the client provided financial statements or balance sheet?
|
||||||
|
- generic [ref=e237]:
|
||||||
|
- generic [ref=e238]:
|
||||||
|
- generic [ref=e239] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e240]
|
||||||
|
- generic [ref=e241]: "Yes"
|
||||||
|
- generic [ref=e242] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e243]
|
||||||
|
- generic [ref=e244]: "No"
|
||||||
|
- generic [ref=e245] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e246]
|
||||||
|
- generic [ref=e247]: N/A
|
||||||
|
- generic [ref=e248]:
|
||||||
|
- generic [ref=e249]: Details (optional)
|
||||||
|
- textbox "Enter details..." [ref=e250]
|
||||||
|
- generic [ref=e251]:
|
||||||
|
- paragraph [ref=e252]: Are the client's financial statements complete and accurate?
|
||||||
|
- generic [ref=e253]:
|
||||||
|
- generic [ref=e254]:
|
||||||
|
- generic [ref=e255] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e256]
|
||||||
|
- generic [ref=e257]: "Yes"
|
||||||
|
- generic [ref=e258] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e259]
|
||||||
|
- generic [ref=e260]: "No"
|
||||||
|
- generic [ref=e261] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e262]
|
||||||
|
- generic [ref=e263]: N/A
|
||||||
|
- generic [ref=e264]:
|
||||||
|
- generic [ref=e265]: Details (optional)
|
||||||
|
- textbox "Enter details..." [ref=e266]
|
||||||
|
- generic [ref=e267]:
|
||||||
|
- heading "Regulatory Compliance" [level=2] [ref=e268]
|
||||||
|
- paragraph [ref=e269]: If you answer yes, you will score 1 point, if you answer no you will score 0 points
|
||||||
|
- generic [ref=e270]:
|
||||||
|
- generic [ref=e271]:
|
||||||
|
- paragraph [ref=e272]: Does the client comply with all relevant regulatory requirements and standards?
|
||||||
|
- generic [ref=e274]:
|
||||||
|
- generic [ref=e275] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e276]
|
||||||
|
- generic [ref=e277]: "Yes"
|
||||||
|
- generic [ref=e278] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e279]
|
||||||
|
- generic [ref=e280]: "No"
|
||||||
|
- generic [ref=e281] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e282]
|
||||||
|
- generic [ref=e283]: N/A
|
||||||
|
- generic [ref=e284]:
|
||||||
|
- paragraph [ref=e285]: The client has no pending legal or regulatory issues that you know of that could impact the audit?
|
||||||
|
- generic [ref=e287]:
|
||||||
|
- generic [ref=e288] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e289]
|
||||||
|
- generic [ref=e290]: "Yes"
|
||||||
|
- generic [ref=e291] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e292]
|
||||||
|
- generic [ref=e293]: "No"
|
||||||
|
- generic [ref=e294] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e295]
|
||||||
|
- generic [ref=e296]: N/A
|
||||||
|
- generic [ref=e297]:
|
||||||
|
- paragraph [ref=e298]: The client has been subject to no regulatory investigations or penalties?
|
||||||
|
- generic [ref=e300]:
|
||||||
|
- generic [ref=e301] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e302]
|
||||||
|
- generic [ref=e303]: "Yes"
|
||||||
|
- generic [ref=e304] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e305]
|
||||||
|
- generic [ref=e306]: "No"
|
||||||
|
- generic [ref=e307] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e308]
|
||||||
|
- generic [ref=e309]: N/A
|
||||||
|
- generic [ref=e310]:
|
||||||
|
- heading "Risk Assessment" [level=2] [ref=e311]
|
||||||
|
- paragraph [ref=e312]: If you answer yes, you will score 1 point, if you answer no you will score 0 points
|
||||||
|
- generic [ref=e313]:
|
||||||
|
- generic [ref=e314]:
|
||||||
|
- paragraph [ref=e315]: There are no key risks associated with the audit?
|
||||||
|
- generic [ref=e317]:
|
||||||
|
- generic [ref=e318] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e319]
|
||||||
|
- generic [ref=e320]: "Yes"
|
||||||
|
- generic [ref=e321] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e322]
|
||||||
|
- generic [ref=e323]: "No"
|
||||||
|
- generic [ref=e324] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e325]
|
||||||
|
- generic [ref=e326]: N/A
|
||||||
|
- generic [ref=e327]:
|
||||||
|
- paragraph [ref=e328]: Have you completed a conflict check?
|
||||||
|
- generic [ref=e329]:
|
||||||
|
- generic [ref=e330]:
|
||||||
|
- generic [ref=e331] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e332]
|
||||||
|
- generic [ref=e333]: "Yes"
|
||||||
|
- generic [ref=e334] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e335]
|
||||||
|
- generic [ref=e336]: "No"
|
||||||
|
- generic [ref=e337] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e338]
|
||||||
|
- generic [ref=e339]: N/A
|
||||||
|
- generic [ref=e340]:
|
||||||
|
- generic [ref=e341]: Details (required)
|
||||||
|
- textbox "Enter details..." [ref=e342]
|
||||||
|
- generic [ref=e343]:
|
||||||
|
- paragraph [ref=e344]: Are you and other BTI member firms independent with the meaning of local and IESBA rules?
|
||||||
|
- generic [ref=e346]:
|
||||||
|
- generic [ref=e347] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e348]
|
||||||
|
- generic [ref=e349]: "Yes"
|
||||||
|
- generic [ref=e350] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e351]
|
||||||
|
- generic [ref=e352]: "No"
|
||||||
|
- generic [ref=e353] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e354]
|
||||||
|
- generic [ref=e355]: N/A
|
||||||
|
- generic [ref=e356]:
|
||||||
|
- heading "Resource Allocation" [level=2] [ref=e357]
|
||||||
|
- paragraph [ref=e358]: If you answer yes, you will score 1 point, if you answer no you will score 0 points
|
||||||
|
- generic [ref=e359]:
|
||||||
|
- generic [ref=e360]:
|
||||||
|
- paragraph [ref=e361]: What resources are required for the audit (personnel, time, budget)?
|
||||||
|
- generic [ref=e362]:
|
||||||
|
- generic [ref=e363]:
|
||||||
|
- generic [ref=e364] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e365]
|
||||||
|
- generic [ref=e366]: "Yes"
|
||||||
|
- generic [ref=e367] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e368]
|
||||||
|
- generic [ref=e369]: "No"
|
||||||
|
- generic [ref=e370] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e371]
|
||||||
|
- generic [ref=e372]: N/A
|
||||||
|
- generic [ref=e373]:
|
||||||
|
- generic [ref=e374]: Details (optional)
|
||||||
|
- textbox "Enter details..." [ref=e375]
|
||||||
|
- generic [ref=e376]:
|
||||||
|
- paragraph [ref=e377]: Does your firm have the scale, seniority and degree of expertise available at the right time to report in accordance with the client's schedule?
|
||||||
|
- generic [ref=e378]:
|
||||||
|
- generic [ref=e379]:
|
||||||
|
- generic [ref=e380] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [ref=e381]
|
||||||
|
- generic [ref=e382]: "Yes"
|
||||||
|
- generic [ref=e383] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e384]
|
||||||
|
- generic [ref=e385]: "No"
|
||||||
|
- generic [ref=e386] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e387]
|
||||||
|
- generic [ref=e388]: N/A
|
||||||
|
- generic [ref=e389]:
|
||||||
|
- generic [ref=e390]: Details (optional)
|
||||||
|
- textbox "Enter details..." [ref=e391]
|
||||||
|
- generic [ref=e392]:
|
||||||
|
- heading "Reporting Requirements" [level=2] [ref=e393]
|
||||||
|
- paragraph [ref=e394]: If you answer yes, you will score 1 point, if you answer no you will score 0 points
|
||||||
|
- generic [ref=e396]:
|
||||||
|
- paragraph [ref=e397]: Do we understand reporting rules, regulatory environment and stakeholder expectations?
|
||||||
|
- generic [ref=e398]:
|
||||||
|
- generic [ref=e399]:
|
||||||
|
- generic [ref=e400] [cursor=pointer]:
|
||||||
|
- radio "Yes" [checked] [active] [ref=e401]
|
||||||
|
- generic [ref=e402]: "Yes"
|
||||||
|
- generic [ref=e403] [cursor=pointer]:
|
||||||
|
- radio "No" [ref=e404]
|
||||||
|
- generic [ref=e405]: "No"
|
||||||
|
- generic [ref=e406] [cursor=pointer]:
|
||||||
|
- radio "N/A" [ref=e407]
|
||||||
|
- generic [ref=e408]: N/A
|
||||||
|
- generic [ref=e409]:
|
||||||
|
- generic [ref=e410]: Details (optional)
|
||||||
|
- textbox "Enter details..." [ref=e411]
|
||||||
|
- generic [ref=e412]:
|
||||||
|
- heading "Additional Comments" [level=2] [ref=e413]
|
||||||
|
- textbox "Enter any additional comments to support your decision..." [ref=e414]
|
||||||
|
- button "Complete" [ref=e416]
|
||||||
126
CLAUDE.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# CLAUDE.md – Go No Go
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Laravel 12 application with Laravel Nova 5 administration panel. The admin panel is served at `/cp`. Frontend users authenticate via Azure AD SSO (Laravel Socialite). Nova uses its built-in authentication.
|
||||||
|
|
||||||
|
- **Framework:** Laravel 12
|
||||||
|
- **Admin Panel:** Laravel Nova 5 at `/cp`
|
||||||
|
- **Auth (frontend):** Laravel Socialite with Azure AD SSO
|
||||||
|
- **Auth (Nova):** Built-in Nova authentication
|
||||||
|
- **Database:** MySQL (`go-no-go`)
|
||||||
|
- **Local URL:** http://go-no-go.test (Laravel Herd)
|
||||||
|
|
||||||
|
## Blaude Execution Flow
|
||||||
|
|
||||||
|
Before starting any task, load application knowledge:
|
||||||
|
|
||||||
|
### Phase 1: Load Application Knowledge
|
||||||
|
|
||||||
|
1. Read `docs/index.md` to understand available documentation
|
||||||
|
2. Based on the task, identify which documentation files are relevant
|
||||||
|
3. Read the relevant documentation files to load context
|
||||||
|
4. Always include docs and rules about sub-agents
|
||||||
|
5. If `docs/index.md` doesn't exist, **STOP** and inform the user
|
||||||
|
|
||||||
|
### Phase 2: Analyze Task Requirements
|
||||||
|
|
||||||
|
1. Parse the task description
|
||||||
|
2. Determine which sub-agents will be needed
|
||||||
|
3. Identify which documentation is most relevant
|
||||||
|
4. Plan the execution approach
|
||||||
|
|
||||||
|
### Phase 3: Execute Task
|
||||||
|
|
||||||
|
1. Use the TodoWrite tool to create a task list
|
||||||
|
2. Launch appropriate sub-agents and let them read the relevant docs found in Phase 1
|
||||||
|
3. Follow async execution patterns (parallel when independent, sequential when dependent)
|
||||||
|
4. Track progress by updating todos
|
||||||
|
|
||||||
|
## Claude Code – Mandatory Sub-Agent Orchestration Rules
|
||||||
|
|
||||||
|
You are operating in a project that supports **specialized sub-agents**.
|
||||||
|
|
||||||
|
These rules are **non-optional**.
|
||||||
|
|
||||||
|
### Core Principle
|
||||||
|
|
||||||
|
**You are an orchestrator, not a solo executor.**
|
||||||
|
|
||||||
|
Whenever a task involves analysis, code changes, verification, or testing, you MUST:
|
||||||
|
- Identify suitable sub-agents
|
||||||
|
- Delegate work to them
|
||||||
|
- Coordinate their outputs
|
||||||
|
- Only integrate results at the end
|
||||||
|
|
||||||
|
### Absolute Stop Rule
|
||||||
|
|
||||||
|
If you are about to write, modify, refactor, or delete code directly, STOP.
|
||||||
|
|
||||||
|
Before making any code change, you MUST:
|
||||||
|
1. Identify available sub-agents
|
||||||
|
2. Decide which sub-agents should perform the work
|
||||||
|
3. Delegate the work to them
|
||||||
|
|
||||||
|
Direct code edits without delegation are considered an incorrect response.
|
||||||
|
|
||||||
|
### Mandatory Workflow
|
||||||
|
|
||||||
|
#### Phase 1: Task Decomposition & Agent Selection
|
||||||
|
|
||||||
|
Before starting:
|
||||||
|
1. Break the task into clear, concrete subtasks
|
||||||
|
2. For each subtask, determine what specialization is required and whether it can run independently
|
||||||
|
3. Assign each subtask to a specialized sub-agent
|
||||||
|
|
||||||
|
You MUST explicitly state which sub-agents are being used and why.
|
||||||
|
|
||||||
|
#### Phase 2: Parallel Execution
|
||||||
|
|
||||||
|
Default assumption: subtasks run in parallel.
|
||||||
|
|
||||||
|
- Launch all independent sub-agents in a single message
|
||||||
|
- Each sub-agent must have a clearly defined scope and explicit file or responsibility boundaries
|
||||||
|
- Sub-agents MUST NOT overlap responsibility unless explicitly required
|
||||||
|
|
||||||
|
Sequential execution is allowed ONLY when a hard dependency exists.
|
||||||
|
|
||||||
|
#### Phase 3: Verification & Synthesis
|
||||||
|
|
||||||
|
After sub-agents complete:
|
||||||
|
1. Verify all delegated tasks completed successfully
|
||||||
|
2. Resolve conflicts or inconsistencies
|
||||||
|
3. Integrate outputs into a coherent result
|
||||||
|
|
||||||
|
For any user-facing or logic-critical change, delegate verification to an appropriate testing or validation sub-agent.
|
||||||
|
|
||||||
|
### Parallelism Rules
|
||||||
|
|
||||||
|
Run in parallel:
|
||||||
|
- Independent file changes
|
||||||
|
- Backend + frontend work
|
||||||
|
- Code implementation + documentation
|
||||||
|
- Multiple exploration queries
|
||||||
|
|
||||||
|
Run sequentially:
|
||||||
|
- Exploration then Implementation
|
||||||
|
- Implementation then Testing
|
||||||
|
- Testing then Fixes then Re-testing
|
||||||
|
|
||||||
|
### Transparency Requirements
|
||||||
|
|
||||||
|
You MUST announce which sub-agents are being used before execution and summarize each sub-agent's contribution after execution.
|
||||||
|
|
||||||
|
## Key Commands
|
||||||
|
|
||||||
|
- `herd php artisan migrate:fresh --seed` – Reset database with seed data
|
||||||
|
- `herd php artisan app:schema-generate` – Regenerate `database/schema.md`
|
||||||
|
- `/blaude_work` – Execute tasks with full application knowledge
|
||||||
|
- `/blaude_update_docs` – Update documentation and index
|
||||||
|
- `/blaude_make_schema` – Generate database schema documentation
|
||||||
|
|
||||||
|
## Test User
|
||||||
|
|
||||||
|
- **Email:** jonathan@blijnder.nl
|
||||||
|
- **Password:** secret
|
||||||
|
- **Auto-login:** `GET /login-jonathan` (local/testing environments only)
|
||||||
134
README.md
@@ -0,0 +1,134 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
```
|
||||||
|
██████╗ ██████╗ ███╗ ██╗ ██████╗ ██████╗ ██████╗
|
||||||
|
██╔════╝ ██╔═══██╗ ████╗ ██║██╔═══██╗ ██╔════╝ ██╔═══██╗
|
||||||
|
██║ ███╗██║ ██║█████╗██╔██╗ ██║██║ ██║█████╗██║ ███╗██║ ██║
|
||||||
|
██║ ██║██║ ██║╚════╝██║╚██╗██║██║ ██║╚════╝██║ ██║██║ ██║
|
||||||
|
╚██████╔╝╚██████╔╝ ██║ ╚████║╚██████╔╝ ╚██████╔╝╚██████╔╝
|
||||||
|
╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
**✨ Laravel 12 + Nova 5 Administration Platform ✨**
|
||||||
|
|
||||||
|
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🎮 GETTING STARTED
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ⚡ QUICK START ⚡ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- 📦 PHP 8.2+
|
||||||
|
- 📦 Laravel Herd
|
||||||
|
- 📦 MySQL
|
||||||
|
- 📦 Composer
|
||||||
|
- 📦 Node.js & npm
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Install dependencies
|
||||||
|
composer install && npm install
|
||||||
|
|
||||||
|
# Step 2: Configure environment
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env: set DB_DATABASE=go-no-go, APP_URL=http://go-no-go.test
|
||||||
|
|
||||||
|
# Step 3: Generate key
|
||||||
|
herd php artisan key:generate
|
||||||
|
|
||||||
|
# Step 4: Run migrations and seed
|
||||||
|
herd php artisan migrate:fresh --seed
|
||||||
|
|
||||||
|
# Step 5: Build assets
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Access
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auto-login as test user (local only)
|
||||||
|
open http://go-no-go.test/login-jonathan
|
||||||
|
|
||||||
|
# Admin panel
|
||||||
|
open http://go-no-go.test/cp
|
||||||
|
```
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<code>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 📺 FEATURES
|
||||||
|
|
||||||
|
- 🔐 **Two-Factor Authentication** – Fortify-powered 2FA for admin accounts
|
||||||
|
- ✉️ **Email Verification** – Required for Nova access
|
||||||
|
- 🛡️ **Nova 5 Admin Panel** – Full administration at `/cp`
|
||||||
|
- 👤 **Test User Seeder** – Quick setup with `JonathanSeeder`
|
||||||
|
- 🚀 **Auto-Login Route** – `/login-jonathan` for local development
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<code>═══════════════════════════════════════════════════════════════</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 📼 DOCUMENTATION
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
|
||||||
|
### 📁 Core Docs
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| [`docs/index.md`](docs/index.md) | Master documentation index |
|
||||||
|
| [`database/schema.md`](database/schema.md) | Database schema reference |
|
||||||
|
| [`CLAUDE.md`](CLAUDE.md) | AI assistant project instructions |
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
|
||||||
|
### 🤖 Sub-Agents
|
||||||
|
| Agent | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `code-style-reviewer` | Code standards enforcement |
|
||||||
|
| `laravel-php-code-writer` | PHP code implementation |
|
||||||
|
| `laravel-nova-code-writer` | Nova resource management |
|
||||||
|
| `phpunit-code-writer` | PHPUnit test creation |
|
||||||
|
| `vue-code-writer` | Vue.js component building |
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<code>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 🌴 CONFIGURATION
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Admin Panel | `/cp` |
|
||||||
|
| Database | `go-no-go` (MySQL) |
|
||||||
|
| Local URL | `http://go-no-go.test` |
|
||||||
|
| Test User | `jonathan@blijnder.nl` / `secret` |
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
`═══════════════════════════════════════════════════════════════`
|
||||||
|
|
||||||
|
**Made with 💜 and mass amounts of ☕**
|
||||||
|
|
||||||
|
*🌴 Stay rad! 🌴*
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
86
app/Console/Commands/DevMenuCommand.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
final class DevMenuCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Developer tools menu';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (! in_array(app()->environment(), ['local', 'testing'])) {
|
||||||
|
$this->error('This command can only be run in local or testing environments.');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('');
|
||||||
|
$this->info(' ╔═══════════════════════════════╗');
|
||||||
|
$this->info(' ║ Go No Go — Dev Tools ║');
|
||||||
|
$this->info(' ╚═══════════════════════════════╝');
|
||||||
|
$this->info('');
|
||||||
|
|
||||||
|
$choice = $this->choice('Select an action', [
|
||||||
|
0 => 'Exit',
|
||||||
|
1 => 'Fresh migrate, seed & build',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($choice === 'Exit') {
|
||||||
|
$this->info('Bye!');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($choice === 'Fresh migrate, seed & build') {
|
||||||
|
$this->freshMigrateAndBuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs migrate:fresh with seeding, then runs npm build.
|
||||||
|
*
|
||||||
|
* Displays output from both processes and confirms success or failure.
|
||||||
|
*/
|
||||||
|
private function freshMigrateAndBuild(): void
|
||||||
|
{
|
||||||
|
$this->info('');
|
||||||
|
$this->comment('Running migrate:fresh --seed...');
|
||||||
|
$this->call('migrate:fresh', ['--seed' => true]);
|
||||||
|
|
||||||
|
$this->info('');
|
||||||
|
$this->comment('Running npm run build...');
|
||||||
|
|
||||||
|
$process = new Process(['npm', 'run', 'build']);
|
||||||
|
$process->setWorkingDirectory(base_path());
|
||||||
|
$process->setTimeout(120);
|
||||||
|
$process->run(function (string $type, string $output): void {
|
||||||
|
$this->output->write($output);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($process->isSuccessful()) {
|
||||||
|
$this->info('');
|
||||||
|
$this->info('Environment rebuilt successfully.');
|
||||||
|
} else {
|
||||||
|
$this->error('Build failed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
app/Console/Commands/GenerateSchemaCommand.php
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
class GenerateSchemaCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'app:schema-generate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Generate a schema.md file documenting all database tables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('Generating database schema documentation...');
|
||||||
|
|
||||||
|
// Get all tables
|
||||||
|
$tables = DB::select('SHOW TABLES');
|
||||||
|
$tableKey = 'Tables_in_'.DB::getDatabaseName();
|
||||||
|
|
||||||
|
$tableNames = [];
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$tableNames[] = $table->$tableKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort tables alphabetically
|
||||||
|
sort($tableNames);
|
||||||
|
|
||||||
|
// Build markdown content
|
||||||
|
$markdown = "# Database Schema Documentation\n\n";
|
||||||
|
$markdown .= '> Generated: '.date('Y-m-d H:i:s')."\n";
|
||||||
|
$markdown .= '> Database: '.DB::getDatabaseName()."\n";
|
||||||
|
$markdown .= '> Total Tables: '.count($tableNames)."\n\n";
|
||||||
|
$markdown .= "## Table of Contents\n\n";
|
||||||
|
|
||||||
|
// Add table of contents
|
||||||
|
foreach ($tableNames as $tableName) {
|
||||||
|
$markdown .= "- [{$tableName}](#{$tableName})\n";
|
||||||
|
}
|
||||||
|
$markdown .= "\n---\n\n";
|
||||||
|
|
||||||
|
// Process each table
|
||||||
|
foreach ($tableNames as $tableName) {
|
||||||
|
$this->line("Processing table: {$tableName}");
|
||||||
|
|
||||||
|
$markdown .= "## {$tableName}\n\n";
|
||||||
|
|
||||||
|
// Get columns
|
||||||
|
$columns = DB::select("SHOW COLUMNS FROM `{$tableName}`");
|
||||||
|
|
||||||
|
// Get foreign keys
|
||||||
|
$foreignKeys = DB::select('
|
||||||
|
SELECT
|
||||||
|
CONSTRAINT_NAME,
|
||||||
|
COLUMN_NAME,
|
||||||
|
REFERENCED_TABLE_NAME,
|
||||||
|
REFERENCED_COLUMN_NAME
|
||||||
|
FROM
|
||||||
|
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||||
|
WHERE
|
||||||
|
TABLE_NAME = ?
|
||||||
|
AND TABLE_SCHEMA = DATABASE()
|
||||||
|
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||||
|
', [$tableName]);
|
||||||
|
|
||||||
|
// Build FK lookup array
|
||||||
|
$fkLookup = [];
|
||||||
|
foreach ($foreignKeys as $fk) {
|
||||||
|
$fkLookup[$fk->COLUMN_NAME] = [
|
||||||
|
'table' => $fk->REFERENCED_TABLE_NAME,
|
||||||
|
'column' => $fk->REFERENCED_COLUMN_NAME,
|
||||||
|
'constraint' => $fk->CONSTRAINT_NAME,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build columns table
|
||||||
|
$markdown .= "| Field | Type | Null | Key | Default | Extra | Foreign Key |\n";
|
||||||
|
$markdown .= "|-------|------|------|-----|---------|-------|-------------|\n";
|
||||||
|
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
$fkInfo = '';
|
||||||
|
if (isset($fkLookup[$column->Field])) {
|
||||||
|
$fk = $fkLookup[$column->Field];
|
||||||
|
$fkInfo = "→ {$fk['table']}.{$fk['column']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$markdown .= "| {$column->Field} | {$column->Type} | {$column->Null} | {$column->Key} | ";
|
||||||
|
$markdown .= ($column->Default === null ? 'NULL' : $column->Default).' | ';
|
||||||
|
$markdown .= "{$column->Extra} | {$fkInfo} |\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add foreign key details if any
|
||||||
|
if (! empty($foreignKeys)) {
|
||||||
|
$markdown .= "\n### Foreign Key Constraints\n\n";
|
||||||
|
foreach ($foreignKeys as $fk) {
|
||||||
|
$markdown .= "- **{$fk->CONSTRAINT_NAME}**: `{$fk->COLUMN_NAME}` → `{$fk->REFERENCED_TABLE_NAME}.{$fk->REFERENCED_COLUMN_NAME}`\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$markdown .= "\n---\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
$directory = base_path('database');
|
||||||
|
if (! File::exists($directory)) {
|
||||||
|
File::makeDirectory($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
$filePath = $directory.'/schema.md';
|
||||||
|
File::put($filePath, $markdown);
|
||||||
|
|
||||||
|
$this->info('✅ Schema documentation generated successfully!');
|
||||||
|
$this->line("📄 File saved to: {$filePath}");
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
app/Http/Controllers/Auth/SocialiteController.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
|
|
||||||
|
final class SocialiteController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Redirect the user to the Azure AD authentication page.
|
||||||
|
*/
|
||||||
|
public function redirect(): RedirectResponse
|
||||||
|
{
|
||||||
|
return Socialite::driver('azure')->redirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the callback from Azure AD after authentication.
|
||||||
|
*/
|
||||||
|
public function callback(): RedirectResponse
|
||||||
|
{
|
||||||
|
$azureUser = Socialite::driver('azure')->user();
|
||||||
|
|
||||||
|
$user = User::query()->updateOrCreate(
|
||||||
|
['email' => $azureUser->getEmail()],
|
||||||
|
[
|
||||||
|
'name' => $azureUser->getName(),
|
||||||
|
'azure_id' => $azureUser->getId(),
|
||||||
|
'photo' => $azureUser->getAvatar(),
|
||||||
|
'job_title' => Arr::get($azureUser->user, 'jobTitle'),
|
||||||
|
'department' => Arr::get($azureUser->user, 'department'),
|
||||||
|
'company_name' => Arr::get($azureUser->user, 'companyName'),
|
||||||
|
'phone' => Arr::get($azureUser->user, 'mobilePhone', Arr::get($azureUser->user, 'businessPhones.0')),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($user->role_id === null) {
|
||||||
|
$user->update(['role_id' => Role::where('name', 'user')->first()->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
auth()->login($user);
|
||||||
|
|
||||||
|
ActivityLogger::log('login', $user->id, metadata: ['email' => $user->email, 'firm_name' => Arr::get($azureUser->user, 'companyName')]);
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the user out and redirect to landing page.
|
||||||
|
*/
|
||||||
|
public function logout(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
ActivityLogger::log('logout', auth()->id());
|
||||||
|
|
||||||
|
auth()->logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
19
app/Http/Controllers/LandingController.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
final class LandingController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the landing page.
|
||||||
|
*/
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('Landing');
|
||||||
|
}
|
||||||
|
}
|
||||||
120
app/Http/Controllers/ScreeningController.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\Screening\UpdateScreeningRequest;
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\Screening;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
final class ScreeningController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new screening session for the authenticated user.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$screening = Screening::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ActivityLogger::log('screening_started', auth()->id());
|
||||||
|
|
||||||
|
return redirect()->route('screening.show', $screening);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the screening questionnaire.
|
||||||
|
*/
|
||||||
|
public function show(Screening $screening): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('Screening/Show', [
|
||||||
|
'screening' => $screening,
|
||||||
|
'questions' => array_values(config('screening.questions')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save screening answers and redirect to result.
|
||||||
|
*/
|
||||||
|
public function update(UpdateScreeningRequest $request, Screening $screening): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$this->saveAnswers($screening, Arr::get($validated, 'answers'));
|
||||||
|
$this->calculateAndUpdateScore($screening, Arr::get($validated, 'answers'));
|
||||||
|
|
||||||
|
ActivityLogger::log('screening_completed', auth()->id(), metadata: ['score' => $screening->score, 'passed' => $screening->passed]);
|
||||||
|
|
||||||
|
return redirect()->route('screening.result', $screening);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the screening result with available categories.
|
||||||
|
*/
|
||||||
|
public function result(Screening $screening): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('Screening/Result', [
|
||||||
|
'screening' => $screening,
|
||||||
|
'passed' => $screening->passed,
|
||||||
|
'score' => $screening->score,
|
||||||
|
'totalQuestions' => count(config('screening.questions')),
|
||||||
|
'categories' => $screening->passed ? Category::orderBy('sort_order')->get(['id', 'name']) : [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save screening answers to the database using upsert pattern.
|
||||||
|
*/
|
||||||
|
private function saveAnswers(Screening $screening, array $answers): void
|
||||||
|
{
|
||||||
|
foreach ($answers as $questionNumber => $value) {
|
||||||
|
$screening->answers()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'screening_id' => $screening->id,
|
||||||
|
'question_number' => (int) $questionNumber,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => $value,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the score and update the screening record.
|
||||||
|
*/
|
||||||
|
private function calculateAndUpdateScore(Screening $screening, array $answers): void
|
||||||
|
{
|
||||||
|
$score = $this->calculateScore($answers);
|
||||||
|
$passed = $score >= config('screening.passing_score', 5);
|
||||||
|
|
||||||
|
$screening->update([
|
||||||
|
'score' => $score,
|
||||||
|
'passed' => $passed,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total score from the answers.
|
||||||
|
*/
|
||||||
|
private function calculateScore(array $answers): int
|
||||||
|
{
|
||||||
|
$score = 0;
|
||||||
|
|
||||||
|
foreach ($answers as $value) {
|
||||||
|
if ($value === 'yes') {
|
||||||
|
$score++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $score;
|
||||||
|
}
|
||||||
|
}
|
||||||
215
app/Http/Controllers/SessionController.php
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\Session\UpdateSessionRequest;
|
||||||
|
use App\Models\Session;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
|
use App\Services\ScoringService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
final class SessionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new session for the selected category.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$session = Session::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'category_id' => $request->input('category_id'),
|
||||||
|
'screening_id' => $request->input('screening_id'),
|
||||||
|
'status' => 'in_progress',
|
||||||
|
]);
|
||||||
|
|
||||||
|
ActivityLogger::log('session_started', auth()->id(), sessionId: $session->id, categoryId: (int) $request->input('category_id'), metadata: ['category_id' => $request->input('category_id')]);
|
||||||
|
|
||||||
|
return redirect()->route('sessions.show', $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the session questionnaire with category, question groups, questions, and existing answers.
|
||||||
|
*/
|
||||||
|
public function show(Session $session): Response
|
||||||
|
{
|
||||||
|
$session->load('category', 'user');
|
||||||
|
|
||||||
|
$questionGroups = $session->category
|
||||||
|
->questionGroups()
|
||||||
|
->with(['questions' => fn ($q) => $q->orderBy('sort_order')])
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
ActivityLogger::log('step_viewed', auth()->id(), sessionId: $session->id, categoryId: $session->category_id, metadata: ['question_group_id' => $questionGroups->first()?->id]);
|
||||||
|
|
||||||
|
$answers = $session->answers()->get()->keyBy('question_id');
|
||||||
|
|
||||||
|
$scoringService = new ScoringService;
|
||||||
|
$score = $scoringService->calculateScore($session);
|
||||||
|
|
||||||
|
return Inertia::render('Session/Show', [
|
||||||
|
'session' => $session,
|
||||||
|
'questionGroups' => $questionGroups,
|
||||||
|
'answers' => $answers,
|
||||||
|
'score' => $score,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save session basic info, answers, and additional comments.
|
||||||
|
*/
|
||||||
|
public function update(UpdateSessionRequest $request, Session $session): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
if (Arr::has($validated, 'answers')) {
|
||||||
|
$this->saveAnswers($session, Arr::get($validated, 'answers'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Arr::has($validated, 'additional_comments')) {
|
||||||
|
$session->update(['additional_comments' => Arr::get($validated, 'additional_comments')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->boolean('complete')) {
|
||||||
|
return $this->completeSession($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or update answers for the session using composite key upsert.
|
||||||
|
*/
|
||||||
|
private function saveAnswers(Session $session, array $answers): void
|
||||||
|
{
|
||||||
|
foreach ($answers as $questionId => $answer) {
|
||||||
|
$session->answers()->updateOrCreate(
|
||||||
|
['question_id' => (int) $questionId],
|
||||||
|
[
|
||||||
|
'value' => Arr::get($answer, 'value'),
|
||||||
|
'text_value' => Arr::get($answer, 'text_value'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
ActivityLogger::log('answer_saved', auth()->id(), sessionId: $session->id, categoryId: $session->category_id, metadata: [
|
||||||
|
'question_id' => (int) $questionId,
|
||||||
|
'value' => Arr::get($answer, 'value'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete the session by calculating final score and result.
|
||||||
|
*/
|
||||||
|
private function completeSession(Session $session): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->validateSessionCompletion($session);
|
||||||
|
|
||||||
|
$scoringService = new ScoringService;
|
||||||
|
$score = $scoringService->calculateScore($session);
|
||||||
|
$result = $scoringService->determineResult($score);
|
||||||
|
|
||||||
|
$session->update([
|
||||||
|
'score' => $score,
|
||||||
|
'result' => $result,
|
||||||
|
'status' => 'completed',
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ActivityLogger::log('session_completed', auth()->id(), sessionId: $session->id, categoryId: $session->category_id, metadata: ['category_id' => $session->category_id, 'score' => $score, 'result' => $result]);
|
||||||
|
|
||||||
|
return redirect()->route('sessions.result', $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that all required fields are answered before session completion.
|
||||||
|
*/
|
||||||
|
private function validateSessionCompletion(Session $session): void
|
||||||
|
{
|
||||||
|
$session->load(['category.questionGroups.questions', 'answers']);
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($session->category->questionGroups as $questionGroup) {
|
||||||
|
foreach ($questionGroup->questions as $question) {
|
||||||
|
$answer = $session->answers->firstWhere('question_id', $question->id);
|
||||||
|
|
||||||
|
$this->validateRadioAnswer($question, $answer, $errors);
|
||||||
|
$this->validateDetailsAnswer($question, $answer, $errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Arr::exists($errors, 0)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'complete' => $errors,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that radio button questions have an answer selected.
|
||||||
|
*/
|
||||||
|
private function validateRadioAnswer($question, $answer, array &$errors): void
|
||||||
|
{
|
||||||
|
$hasRadioButtons = $question->has_yes || $question->has_no || $question->has_na;
|
||||||
|
|
||||||
|
if ($hasRadioButtons && (! $answer || $answer->value === null)) {
|
||||||
|
$errors[] = "Question '{$question->text}' requires an answer.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that questions with required details have text values provided.
|
||||||
|
*/
|
||||||
|
private function validateDetailsAnswer($question, $answer, array &$errors): void
|
||||||
|
{
|
||||||
|
$details = $question->details;
|
||||||
|
$hasRadioButtons = $question->has_yes || $question->has_no || $question->has_na;
|
||||||
|
|
||||||
|
if ($details === 'required') {
|
||||||
|
if (! $answer || empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) {
|
||||||
|
$errors[] = "Question '{$question->text}' requires details to be provided.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($details === 'req_on_yes' && $answer && $answer->value === 'yes') {
|
||||||
|
if (empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) {
|
||||||
|
$errors[] = "Question '{$question->text}' requires details when answered 'Yes'.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($details === 'req_on_no' && $answer && $answer->value === 'no') {
|
||||||
|
if (empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) {
|
||||||
|
$errors[] = "Question '{$question->text}' requires details when answered 'No'.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $hasRadioButtons && $details !== null && $details !== '' && $details !== 'optional') {
|
||||||
|
if (! $answer || empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) {
|
||||||
|
$errors[] = "Question '{$question->text}' requires a text response.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the final session result.
|
||||||
|
*/
|
||||||
|
public function result(Session $session): Response
|
||||||
|
{
|
||||||
|
$session->load('category');
|
||||||
|
|
||||||
|
return Inertia::render('Session/Result', [
|
||||||
|
'session' => $session,
|
||||||
|
'score' => $session->score,
|
||||||
|
'result' => $session->result,
|
||||||
|
'categoryName' => $session->category->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Inertia\Middleware;
|
||||||
|
use Laravel\Nova\Nova;
|
||||||
|
|
||||||
|
final class HandleInertiaRequests extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The root template that is loaded on the first page visit.
|
||||||
|
*/
|
||||||
|
protected $rootView = 'app';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the current asset version.
|
||||||
|
*/
|
||||||
|
public function version(Request $request): ?string
|
||||||
|
{
|
||||||
|
return parent::version($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the props that are shared by default.
|
||||||
|
*/
|
||||||
|
public function share(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...parent::share($request),
|
||||||
|
'auth' => [
|
||||||
|
'user' => $this->getAuthenticatedUser(),
|
||||||
|
'logo_href' => $this->getLogoHref(),
|
||||||
|
],
|
||||||
|
'flash' => [
|
||||||
|
'success' => fn () => Arr::get($request->session()->all(), 'success'),
|
||||||
|
'error' => fn () => Arr::get($request->session()->all(), 'error'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authenticated user data for frontend.
|
||||||
|
*/
|
||||||
|
private function getAuthenticatedUser(): ?array
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if ($user === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => $user->name,
|
||||||
|
'email' => $user->email,
|
||||||
|
'job_title' => $user->job_title,
|
||||||
|
'company_name' => $user->company_name,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine logo href based on user Nova access.
|
||||||
|
*/
|
||||||
|
private function getLogoHref(): string
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if ($user !== null && Gate::allows('viewNova', $user)) {
|
||||||
|
return Nova::path();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Http/Requests/Screening/UpdateScreeningRequest.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Screening;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class UpdateScreeningRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->route('screening')->user_id === auth()->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'answers' => ['required', 'array', 'size:10'],
|
||||||
|
'answers.*' => ['required', 'string', 'in:yes,no'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom validation messages.
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'answers.required' => 'All screening questions must be answered.',
|
||||||
|
'answers.array' => 'Answers must be provided as an array.',
|
||||||
|
'answers.size' => 'All 10 screening questions must be answered.',
|
||||||
|
'answers.*.required' => 'Each screening question must have an answer.',
|
||||||
|
'answers.*.string' => 'Each answer must be a valid text value.',
|
||||||
|
'answers.*.in' => 'Each answer must be either "yes" or "no".',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Http/Requests/Session/UpdateSessionRequest.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Session;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class UpdateSessionRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->route('session')->user_id === auth()->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'answers' => ['sometimes', 'array'],
|
||||||
|
'answers.*.value' => ['nullable', 'string', 'in:yes,no,not_applicable'],
|
||||||
|
'answers.*.text_value' => ['nullable', 'string', 'max:10000'],
|
||||||
|
'additional_comments' => ['sometimes', 'nullable', 'string', 'max:10000'],
|
||||||
|
'complete' => ['sometimes', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom validation messages.
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'answers.array' => 'Answers must be a valid data structure.',
|
||||||
|
'answers.*.value.in' => 'Answer value must be yes, no, or not_applicable.',
|
||||||
|
'answers.*.text_value.string' => 'Answer text must be text.',
|
||||||
|
'answers.*.text_value.max' => 'Answer text cannot exceed 10000 characters.',
|
||||||
|
'additional_comments.string' => 'Additional comments must be text.',
|
||||||
|
'additional_comments.max' => 'Additional comments cannot exceed 10000 characters.',
|
||||||
|
'complete.boolean' => 'The complete flag must be true or false.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/Models/Answer.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
final class Answer extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fillable attributes for mass assignment.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'session_id',
|
||||||
|
'question_id',
|
||||||
|
'value',
|
||||||
|
'text_value',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast attributes to specific types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'session_id' => 'integer',
|
||||||
|
'question_id' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the session that owns this answer.
|
||||||
|
*/
|
||||||
|
public function session(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Session::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the question that this answer belongs to.
|
||||||
|
*/
|
||||||
|
public function question(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Question::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Models/Category.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
final class Category extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fillable attributes for mass assignment.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast attributes to specific types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all question groups for this category.
|
||||||
|
*/
|
||||||
|
public function questionGroups(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(QuestionGroup::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all sessions for this category.
|
||||||
|
*/
|
||||||
|
public function sessions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Session::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
app/Models/Log.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
final class Log extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the updated_at timestamp for append-only logs.
|
||||||
|
*/
|
||||||
|
public const UPDATED_AT = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fillable attributes for mass assignment.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'session_id',
|
||||||
|
'category_id',
|
||||||
|
'action',
|
||||||
|
'metadata',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast attributes to specific types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => 'integer',
|
||||||
|
'session_id' => 'integer',
|
||||||
|
'category_id' => 'integer',
|
||||||
|
'metadata' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the model and prevent updates and deletes.
|
||||||
|
*/
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
self::updating(function (): bool {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
self::deleting(function (): bool {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that performed the logged action.
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the session associated with this log entry.
|
||||||
|
*/
|
||||||
|
public function session(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Session::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the category associated with this log entry.
|
||||||
|
*/
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
app/Models/Question.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
final class Question extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fillable attributes for mass assignment.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'question_group_id',
|
||||||
|
'text',
|
||||||
|
'has_yes',
|
||||||
|
'has_no',
|
||||||
|
'has_na',
|
||||||
|
'details',
|
||||||
|
'sort_order',
|
||||||
|
'is_scored',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast attributes to specific types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'question_group_id' => 'integer',
|
||||||
|
'has_yes' => 'boolean',
|
||||||
|
'has_no' => 'boolean',
|
||||||
|
'has_na' => 'boolean',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
'is_scored' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the question group that owns this question.
|
||||||
|
*/
|
||||||
|
public function questionGroup(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(QuestionGroup::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all answers for this question.
|
||||||
|
*/
|
||||||
|
public function answers(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Answer::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Models/QuestionGroup.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
final class QuestionGroup extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fillable attributes for mass assignment.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'category_id',
|
||||||
|
'name',
|
||||||
|
'sort_order',
|
||||||
|
'description',
|
||||||
|
'scoring_instructions',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast attributes to specific types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'category_id' => 'integer',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the category that owns this question group.
|
||||||
|
*/
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all questions for this question group.
|
||||||
|
*/
|
||||||
|
public function questions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Question::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Models/Role.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
final class Role extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all users with this role.
|
||||||
|
*/
|
||||||
|
public function users(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
app/Models/Screening.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
final class Screening extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fillable attributes for mass assignment.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'score',
|
||||||
|
'passed',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast attributes to specific types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => 'integer',
|
||||||
|
'score' => 'integer',
|
||||||
|
'passed' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that owns this screening.
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all answers for this screening.
|
||||||
|
*/
|
||||||
|
public function answers(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ScreeningAnswer::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all sessions that reference this screening.
|
||||||
|
*/
|
||||||
|
public function sessions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Session::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Models/ScreeningAnswer.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
final class ScreeningAnswer extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fillable attributes for mass assignment.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'screening_id',
|
||||||
|
'question_number',
|
||||||
|
'value',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast attributes to specific types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'screening_id' => 'integer',
|
||||||
|
'question_number' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the screening that owns this answer.
|
||||||
|
*/
|
||||||
|
public function screening(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Screening::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
app/Models/Session.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
final class Session extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'questionnaire_sessions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fillable attributes for mass assignment.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'category_id',
|
||||||
|
'screening_id',
|
||||||
|
'status',
|
||||||
|
'score',
|
||||||
|
'result',
|
||||||
|
'additional_comments',
|
||||||
|
'completed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast attributes to specific types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => 'integer',
|
||||||
|
'category_id' => 'integer',
|
||||||
|
'screening_id' => 'integer',
|
||||||
|
'score' => 'integer',
|
||||||
|
'completed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that owns this session.
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the category for this session.
|
||||||
|
*/
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the screening that preceded this session.
|
||||||
|
*/
|
||||||
|
public function screening(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Screening::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all answers for this session.
|
||||||
|
*/
|
||||||
|
public function answers(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Answer::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all logs for this session.
|
||||||
|
*/
|
||||||
|
public function logs(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Log::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
app/Models/User.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
final class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'azure_id',
|
||||||
|
'photo',
|
||||||
|
'job_title',
|
||||||
|
'department',
|
||||||
|
'company_name',
|
||||||
|
'phone',
|
||||||
|
'role_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
'azure_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
'role_id' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the role assigned to this user.
|
||||||
|
*/
|
||||||
|
public function role(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Role::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all sessions for this user.
|
||||||
|
*/
|
||||||
|
public function sessions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Session::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all screenings for this user.
|
||||||
|
*/
|
||||||
|
public function screenings(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Screening::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all logs for this user.
|
||||||
|
*/
|
||||||
|
public function logs(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Log::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Nova/Actions/DownloadExcel.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova\Actions;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Maatwebsite\LaravelNovaExcel\Actions\DownloadExcel as BaseDownloadExcel;
|
||||||
|
|
||||||
|
// Fixes Nova 5 incompatibility where field names are PendingTranslation objects instead of strings.
|
||||||
|
final class DownloadExcel extends BaseDownloadExcel
|
||||||
|
{
|
||||||
|
protected $onlyIndexFields = false;
|
||||||
|
/**
|
||||||
|
* @param Model|mixed $row
|
||||||
|
*/
|
||||||
|
public function map($row): array
|
||||||
|
{
|
||||||
|
$only = array_map('strval', $this->getOnly());
|
||||||
|
$except = $this->getExcept();
|
||||||
|
|
||||||
|
if ($row instanceof Model) {
|
||||||
|
if (!$this->onlyIndexFields && $except === null && (!is_array($only) || count($only) === 0)) {
|
||||||
|
$except = $row->getHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
$row->setHidden([]);
|
||||||
|
$row = $this->replaceFieldValuesWhenOnResource($row, $only);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($only) && count($only) > 0) {
|
||||||
|
$row = Arr::only($row, $only);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($except) && count($except) > 0) {
|
||||||
|
$row = Arr::except($row, $except);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function replaceFieldValuesWhenOnResource(Model $model, array $only = []): array
|
||||||
|
{
|
||||||
|
$resource = $this->resolveResource($model);
|
||||||
|
$fields = $this->resourceFields($resource);
|
||||||
|
|
||||||
|
$row = [];
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (!$this->isExportableField($field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\in_array($field->attribute, $only, true)) {
|
||||||
|
$row[$field->attribute] = $field->value;
|
||||||
|
} elseif (\in_array((string) $field->name, $only, true)) {
|
||||||
|
$row[(string) $field->name] = $field->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_diff($only, array_keys($row)) as $attribute) {
|
||||||
|
if ($model->{$attribute}) {
|
||||||
|
$row[$attribute] = $model->{$attribute};
|
||||||
|
} else {
|
||||||
|
$row[$attribute] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = array_merge(array_flip($only), $row);
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
158
app/Nova/AnswerResource.php
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
|
use Laravel\Nova\Fields\DateTime;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Text;
|
||||||
|
use Laravel\Nova\Fields\Textarea;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
|
||||||
|
final class AnswerResource extends Resource
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model the resource corresponds to.
|
||||||
|
*
|
||||||
|
* @var class-string<\App\Models\Answer>
|
||||||
|
*/
|
||||||
|
public static string $model = \App\Models\Answer::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single value that should be used to represent the resource when being displayed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $title = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns that should be searched.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $search = ['id', 'value'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the resource should be displayed in the sidebar.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $displayInNavigation = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationships that should be eager loaded on index queries.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $with = ['session', 'question'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable label of the resource.
|
||||||
|
*/
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Answers';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable singular label of the resource.
|
||||||
|
*/
|
||||||
|
public static function singularLabel(): string
|
||||||
|
{
|
||||||
|
return 'Answer';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fields displayed by the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Fields\Field|\Laravel\Nova\Panel|\Laravel\Nova\ResourceTool|\Illuminate\Http\Resources\MergeValue>
|
||||||
|
*/
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
BelongsTo::make('Session', 'session', SessionResource::class)
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->readonly()
|
||||||
|
->help('The questionnaire session this answer belongs to.'),
|
||||||
|
|
||||||
|
BelongsTo::make('Question', 'question', QuestionResource::class)
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->readonly()
|
||||||
|
->help('The question that was answered.'),
|
||||||
|
|
||||||
|
Text::make('Value')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->readonly()
|
||||||
|
->help('The selected answer: "yes", "no", or "not_applicable". Empty for open text questions.'),
|
||||||
|
|
||||||
|
Textarea::make('Text Value')
|
||||||
|
->alwaysShow()
|
||||||
|
->readonly()
|
||||||
|
->help('Any written details or free text the user provided for this question.'),
|
||||||
|
|
||||||
|
DateTime::make('Created At')
|
||||||
|
->exceptOnForms()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('When this answer was first saved.'),
|
||||||
|
|
||||||
|
DateTime::make('Updated At')
|
||||||
|
->exceptOnForms()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('When this answer was last changed.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cards available for the request.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Filters\Filter>
|
||||||
|
*/
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lenses available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Lenses\Lens>
|
||||||
|
*/
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actions available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Actions\Action>
|
||||||
|
*/
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new DownloadExcel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
132
app/Nova/CategoryResource.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Laravel\Nova\Fields\DateTime;
|
||||||
|
use Laravel\Nova\Fields\HasMany;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Number;
|
||||||
|
use Laravel\Nova\Fields\Text;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
|
||||||
|
final class CategoryResource extends Resource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The model the resource corresponds to.
|
||||||
|
*
|
||||||
|
* @var class-string<\App\Models\Category>
|
||||||
|
*/
|
||||||
|
public static string $model = \App\Models\Category::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single value that should be used to represent the resource when being displayed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $title = 'name';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns that should be searched.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $search = ['id', 'name'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the resource should be displayed in the sidebar.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $displayInNavigation = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable label of the resource.
|
||||||
|
*/
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Categories';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable singular label of the resource.
|
||||||
|
*/
|
||||||
|
public static function singularLabel(): string
|
||||||
|
{
|
||||||
|
return 'Category';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fields displayed by the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Fields\Field|\Laravel\Nova\Panel|\Laravel\Nova\ResourceTool|\Illuminate\Http\Resources\MergeValue>
|
||||||
|
*/
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
Text::make('Name')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->help('The name of this assessment category, such as Audit, Tax, or Legal.')
|
||||||
|
->rules('required', 'max:255'),
|
||||||
|
|
||||||
|
Number::make('Sort Order')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->help('Controls the display order of categories. Lower numbers appear first.')
|
||||||
|
->rules('required', 'integer'),
|
||||||
|
|
||||||
|
HasMany::make('Question Groups', 'questionGroups', QuestionGroupResource::class),
|
||||||
|
|
||||||
|
HasMany::make('Sessions', 'sessions', SessionResource::class),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cards available for the request.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Filters\Filter>
|
||||||
|
*/
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lenses available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Lenses\Lens>
|
||||||
|
*/
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actions available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Actions\Action>
|
||||||
|
*/
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new DownloadExcel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Nova/Dashboards/Main.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova\Dashboards;
|
||||||
|
|
||||||
|
use App\Nova\Metrics\ScreeningsTrend;
|
||||||
|
use App\Nova\Metrics\SessionsTrend;
|
||||||
|
use App\Nova\Metrics\TotalScreenings;
|
||||||
|
use App\Nova\Metrics\TotalSessions;
|
||||||
|
use Laravel\Nova\Dashboards\Main as Dashboard;
|
||||||
|
|
||||||
|
final class Main extends Dashboard
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the cards for the dashboard.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TotalSessions,
|
||||||
|
new TotalScreenings,
|
||||||
|
new SessionsTrend,
|
||||||
|
new ScreeningsTrend,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
167
app/Nova/LogResource.php
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
|
use Laravel\Nova\Fields\Code;
|
||||||
|
use Laravel\Nova\Fields\DateTime;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Text;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
|
||||||
|
final class LogResource extends Resource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The model the resource corresponds to.
|
||||||
|
*
|
||||||
|
* @var class-string<\App\Models\Log>
|
||||||
|
*/
|
||||||
|
public static string $model = \App\Models\Log::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single value that should be used to represent the resource when being displayed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $title = 'action';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns that should be searched.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $search = ['id', 'action'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the resource should be displayed in the sidebar.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $displayInNavigation = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The group associated with the resource.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $group = 'Analytics';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationships that should be eager loaded on index queries.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $with = ['user', 'session', 'category'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable label of the resource.
|
||||||
|
*/
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Logs';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable singular label of the resource.
|
||||||
|
*/
|
||||||
|
public static function singularLabel(): string
|
||||||
|
{
|
||||||
|
return 'Log';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fields displayed by the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Fields\Field|\Laravel\Nova\Panel|\Laravel\Nova\ResourceTool|\Illuminate\Http\Resources\MergeValue>
|
||||||
|
*/
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
BelongsTo::make('User', 'user', User::class)
|
||||||
|
->nullable()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('nullable')
|
||||||
|
->help('The user who performed this action. May be empty for system events.'),
|
||||||
|
|
||||||
|
BelongsTo::make('Session', 'session', SessionResource::class)
|
||||||
|
->nullable()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('nullable')
|
||||||
|
->help('The questionnaire session related to this action, if any.'),
|
||||||
|
|
||||||
|
BelongsTo::make('Category', 'category', CategoryResource::class)
|
||||||
|
->nullable()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('nullable')
|
||||||
|
->help('The assessment category related to this action, if any.'),
|
||||||
|
|
||||||
|
Text::make('Action')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->rules('required', 'max:255')
|
||||||
|
->help('What happened, e.g. "login", "session_started", "answer_saved", "screening_completed".'),
|
||||||
|
|
||||||
|
Code::make('Metadata')
|
||||||
|
->json()
|
||||||
|
->rules('nullable')
|
||||||
|
->help('Additional details about this action in a structured format.'),
|
||||||
|
|
||||||
|
DateTime::make('Created At')
|
||||||
|
->exceptOnForms()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('When this action occurred.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cards available for the request.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Filters\Filter>
|
||||||
|
*/
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lenses available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Lenses\Lens>
|
||||||
|
*/
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actions available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Actions\Action>
|
||||||
|
*/
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new DownloadExcel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Nova/Metrics/ScreeningsTrend.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova\Metrics;
|
||||||
|
|
||||||
|
use App\Models\Screening;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use Laravel\Nova\Metrics\Trend;
|
||||||
|
|
||||||
|
final class ScreeningsTrend extends Trend
|
||||||
|
{
|
||||||
|
public ?int $cacheFor = null;
|
||||||
|
|
||||||
|
public function calculate(NovaRequest $request): mixed
|
||||||
|
{
|
||||||
|
return $this->countByDays($request, Screening::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'Screenings';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ranges(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
30 => '30 Days',
|
||||||
|
60 => '60 Days',
|
||||||
|
90 => '90 Days',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Nova/Metrics/SessionsTrend.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova\Metrics;
|
||||||
|
|
||||||
|
use App\Models\Session;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use Laravel\Nova\Metrics\Trend;
|
||||||
|
|
||||||
|
final class SessionsTrend extends Trend
|
||||||
|
{
|
||||||
|
public function calculate(NovaRequest $request): mixed
|
||||||
|
{
|
||||||
|
return $this->countByDays($request, Session::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'Sessions';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ranges(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
30 => '30 Days',
|
||||||
|
60 => '60 Days',
|
||||||
|
90 => '90 Days',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public $cacheFor = null;
|
||||||
|
}
|
||||||
47
app/Nova/Metrics/TotalScreenings.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova\Metrics;
|
||||||
|
|
||||||
|
use App\Models\Screening;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use Laravel\Nova\Metrics\Value;
|
||||||
|
|
||||||
|
final class TotalScreenings extends Value
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calculate the value of the metric.
|
||||||
|
*/
|
||||||
|
public function calculate(NovaRequest $request): mixed
|
||||||
|
{
|
||||||
|
return $this->count($request, Screening::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable name of the metric.
|
||||||
|
*/
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'Total Screenings';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ranges available for the metric.
|
||||||
|
*/
|
||||||
|
public function ranges(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
30 => '30 Days',
|
||||||
|
60 => '60 Days',
|
||||||
|
365 => '365 Days',
|
||||||
|
'TODAY' => 'Today',
|
||||||
|
'ALL' => 'All Time',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the amount of time the results should be cached.
|
||||||
|
*/
|
||||||
|
public $cacheFor = null;
|
||||||
|
}
|
||||||
35
app/Nova/Metrics/TotalSessions.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova\Metrics;
|
||||||
|
|
||||||
|
use App\Models\Session;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use Laravel\Nova\Metrics\Value;
|
||||||
|
|
||||||
|
final class TotalSessions extends Value
|
||||||
|
{
|
||||||
|
public function calculate(NovaRequest $request): mixed
|
||||||
|
{
|
||||||
|
return $this->count($request, Session::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'Total Sessions';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ranges(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
30 => '30 Days',
|
||||||
|
60 => '60 Days',
|
||||||
|
365 => '365 Days',
|
||||||
|
'TODAY' => 'Today',
|
||||||
|
'ALL' => 'All Time',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public $cacheFor = null;
|
||||||
|
}
|
||||||
146
app/Nova/QuestionGroupResource.php
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
|
use Laravel\Nova\Fields\DateTime;
|
||||||
|
use Laravel\Nova\Fields\HasMany;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Number;
|
||||||
|
use Laravel\Nova\Fields\Text;
|
||||||
|
use Laravel\Nova\Fields\Textarea;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
|
||||||
|
final class QuestionGroupResource extends Resource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The model the resource corresponds to.
|
||||||
|
*
|
||||||
|
* @var class-string<\App\Models\QuestionGroup>
|
||||||
|
*/
|
||||||
|
public static string $model = \App\Models\QuestionGroup::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single value that should be used to represent the resource when being displayed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $title = 'name';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns that should be searched.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $search = ['id', 'name'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the resource should be displayed in the sidebar.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $displayInNavigation = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable label of the resource.
|
||||||
|
*/
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Question Groups';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable singular label of the resource.
|
||||||
|
*/
|
||||||
|
public static function singularLabel(): string
|
||||||
|
{
|
||||||
|
return 'Question Group';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fields displayed by the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Fields\Field|\Laravel\Nova\Panel|\Laravel\Nova\ResourceTool|\Illuminate\Http\Resources\MergeValue>
|
||||||
|
*/
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
BelongsTo::make('Category', 'category', CategoryResource::class)
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('required')
|
||||||
|
->help('The assessment category this group of questions belongs to, such as Audit or Tax.'),
|
||||||
|
|
||||||
|
Text::make('Name')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->rules('required', 'max:255')
|
||||||
|
->help('The title of this question group, shown as a section heading in the questionnaire.'),
|
||||||
|
|
||||||
|
Number::make('Sort Order')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->rules('required', 'integer')
|
||||||
|
->help('Controls the display order within the category. Lower numbers appear first.'),
|
||||||
|
|
||||||
|
Textarea::make('Description')
|
||||||
|
->rules('nullable')
|
||||||
|
->help('An optional description shown to users at the top of this question group.'),
|
||||||
|
|
||||||
|
Textarea::make('Scoring Instructions')
|
||||||
|
->rules('nullable')
|
||||||
|
->help('Optional instructions shown to users explaining how this section is scored, e.g. "If you answer yes, you will score 1 point."'),
|
||||||
|
|
||||||
|
HasMany::make('Questions', 'questions', QuestionResource::class),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cards available for the request.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Filters\Filter>
|
||||||
|
*/
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lenses available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Lenses\Lens>
|
||||||
|
*/
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actions available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Actions\Action>
|
||||||
|
*/
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new DownloadExcel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
183
app/Nova/QuestionResource.php
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
|
use Laravel\Nova\Fields\Boolean;
|
||||||
|
use Laravel\Nova\Fields\DateTime;
|
||||||
|
use Laravel\Nova\Fields\HasMany;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Number;
|
||||||
|
use Laravel\Nova\Fields\Select;
|
||||||
|
use Laravel\Nova\Fields\Text;
|
||||||
|
use Laravel\Nova\Fields\Textarea;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
|
||||||
|
final class QuestionResource extends Resource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The model the resource corresponds to.
|
||||||
|
*
|
||||||
|
* @var class-string<\App\Models\Question>
|
||||||
|
*/
|
||||||
|
public static string $model = \App\Models\Question::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single value that should be used to represent the resource when being displayed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $title = 'text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns that should be searched.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $search = ['id', 'text'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the resource should be displayed in the sidebar.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $displayInNavigation = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The group associated with the resource.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $group = 'Questionnaire';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable label of the resource.
|
||||||
|
*/
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Questions';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable singular label of the resource.
|
||||||
|
*/
|
||||||
|
public static function singularLabel(): string
|
||||||
|
{
|
||||||
|
return 'Question';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fields displayed by the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Fields\Field|\Laravel\Nova\Panel|\Laravel\Nova\ResourceTool|\Illuminate\Http\Resources\MergeValue>
|
||||||
|
*/
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
Text::make('Question', 'text')
|
||||||
|
->displayUsing(fn ($value) => Str::limit($value, 40))
|
||||||
|
->onlyOnIndex()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
BelongsTo::make('Question Group', 'questionGroup', QuestionGroupResource::class)
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('The group this question belongs to. Questions are shown together by group in the questionnaire.'),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Textarea::make('Text')
|
||||||
|
->rules('required')
|
||||||
|
->updateRules('required')
|
||||||
|
->help('The full question text shown to the user in the questionnaire.'),
|
||||||
|
|
||||||
|
Boolean::make('Has Yes')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('When enabled, a "Yes" answer option is shown for this question.'),
|
||||||
|
|
||||||
|
Boolean::make('Has No')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('When enabled, a "No" answer option is shown for this question.'),
|
||||||
|
|
||||||
|
Boolean::make('Has NA', 'has_na')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('When enabled, a "Not Applicable" answer option is shown for this question.'),
|
||||||
|
|
||||||
|
Select::make('Details')
|
||||||
|
->options([
|
||||||
|
'optional' => 'Optional',
|
||||||
|
'required' => 'Required',
|
||||||
|
'req_on_yes' => 'Required on Yes',
|
||||||
|
'req_on_no' => 'Required on No',
|
||||||
|
])
|
||||||
|
->displayUsingLabels()
|
||||||
|
->nullable()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('Controls when the user is asked for additional details. "Required" always asks, "Optional" lets the user choose, "Required on Yes/No" only asks when that answer is selected.'),
|
||||||
|
|
||||||
|
Number::make('Sort Order')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('Controls the display order within the question group. Lower numbers appear first.'),
|
||||||
|
|
||||||
|
Boolean::make('Is Scored')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('When enabled, this question counts toward the total score. A "Yes" answer scores 1 point.'),
|
||||||
|
|
||||||
|
HasMany::make('Answers', 'answers', AnswerResource::class),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cards available for the request.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Filters\Filter>
|
||||||
|
*/
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lenses available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Lenses\Lens>
|
||||||
|
*/
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actions available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Actions\Action>
|
||||||
|
*/
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new DownloadExcel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Nova/Resource.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Database\Eloquent\Builder;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use Laravel\Nova\Resource as NovaResource;
|
||||||
|
use Laravel\Scout\Builder as ScoutBuilder;
|
||||||
|
|
||||||
|
abstract class Resource extends NovaResource
|
||||||
|
{
|
||||||
|
public static function perPageOptions()
|
||||||
|
{
|
||||||
|
return [50, 100, 150];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function perPageViaRelationshipOptions()
|
||||||
|
{
|
||||||
|
return [10, 25, 50];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an "index" query for the given resource.
|
||||||
|
*/
|
||||||
|
public static function indexQuery(NovaRequest $request, Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a Scout search query for the given resource.
|
||||||
|
*/
|
||||||
|
public static function scoutQuery(NovaRequest $request, ScoutBuilder $query): ScoutBuilder
|
||||||
|
{
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a "detail" query for the given resource.
|
||||||
|
*/
|
||||||
|
public static function detailQuery(NovaRequest $request, Builder $query): Builder
|
||||||
|
{
|
||||||
|
return parent::detailQuery($request, $query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a "relatable" query for the given resource.
|
||||||
|
*
|
||||||
|
* This query determines which instances of the model may be attached to other resources.
|
||||||
|
*/
|
||||||
|
public static function relatableQuery(NovaRequest $request, Builder $query): Builder
|
||||||
|
{
|
||||||
|
return parent::relatableQuery($request, $query);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
app/Nova/RoleResource.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Laravel\Nova\Fields\DateTime;
|
||||||
|
use Laravel\Nova\Fields\HasMany;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Text;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
|
||||||
|
final class RoleResource extends Resource
|
||||||
|
{
|
||||||
|
public static string $model = \App\Models\Role::class;
|
||||||
|
|
||||||
|
public static $title = 'name';
|
||||||
|
|
||||||
|
public static $search = ['id', 'name'];
|
||||||
|
|
||||||
|
public static $displayInNavigation = false;
|
||||||
|
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Roles';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function singularLabel(): string
|
||||||
|
{
|
||||||
|
return 'Role';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
Text::make('Name')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->rules('required', 'max:255'),
|
||||||
|
|
||||||
|
DateTime::make('Created At')
|
||||||
|
->exceptOnForms()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
DateTime::make('Updated At')
|
||||||
|
->exceptOnForms()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
HasMany::make('Users', 'users', User::class),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
156
app/Nova/ScreeningResource.php
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
|
use Laravel\Nova\Fields\Boolean;
|
||||||
|
use Laravel\Nova\Fields\DateTime;
|
||||||
|
use Laravel\Nova\Fields\HasMany;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Number;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
|
||||||
|
final class ScreeningResource extends Resource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The model the resource corresponds to.
|
||||||
|
*
|
||||||
|
* @var class-string<\App\Models\Screening>
|
||||||
|
*/
|
||||||
|
public static string $model = \App\Models\Screening::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single value that should be used to represent the resource when being displayed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $title = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns that should be searched.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $search = ['id'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the resource should be displayed in the sidebar.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $displayInNavigation = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The group associated with the resource.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $group = 'Questionnaire';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationships that should be eager loaded on index queries.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $with = ['user'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable label of the resource.
|
||||||
|
*/
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Screenings';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable singular label of the resource.
|
||||||
|
*/
|
||||||
|
public static function singularLabel(): string
|
||||||
|
{
|
||||||
|
return 'Screening';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fields displayed by the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Fields\Field|\Laravel\Nova\Panel|\Laravel\Nova\ResourceTool|\Illuminate\Http\Resources\MergeValue>
|
||||||
|
*/
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
BelongsTo::make('User', 'user', User::class)
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('required')
|
||||||
|
->help('The person who completed this pre-screening.'),
|
||||||
|
|
||||||
|
Number::make('Score')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->rules('required', 'integer')
|
||||||
|
->help('The number of "Yes" answers out of 10 pre-screening questions.'),
|
||||||
|
|
||||||
|
Boolean::make('Passed')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('required', 'boolean')
|
||||||
|
->help('Whether the user scored 5 or more points and was allowed to continue to the full questionnaire.'),
|
||||||
|
|
||||||
|
DateTime::make('Created At')
|
||||||
|
->exceptOnForms()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('When this pre-screening was started.'),
|
||||||
|
|
||||||
|
HasMany::make('Sessions', 'sessions', SessionResource::class),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cards available for the request.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Filters\Filter>
|
||||||
|
*/
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lenses available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Lenses\Lens>
|
||||||
|
*/
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actions available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Actions\Action>
|
||||||
|
*/
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new DownloadExcel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
192
app/Nova/SessionResource.php
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
|
use Laravel\Nova\Fields\DateTime;
|
||||||
|
use Laravel\Nova\Fields\HasMany;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Number;
|
||||||
|
use Laravel\Nova\Fields\Select;
|
||||||
|
use Laravel\Nova\Fields\Textarea;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
|
||||||
|
final class SessionResource extends Resource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The model the resource corresponds to.
|
||||||
|
*
|
||||||
|
* @var class-string<\App\Models\Session>
|
||||||
|
*/
|
||||||
|
public static string $model = \App\Models\Session::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single value that should be used to represent the resource when being displayed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $title = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns that should be searched.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $search = ['id', 'status', 'result'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the resource should be displayed in the sidebar.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $displayInNavigation = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The group associated with the resource.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $group = 'Questionnaire';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationships that should be eager loaded on index queries.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $with = ['user', 'category', 'screening'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable label of the resource.
|
||||||
|
*/
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Sessions';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the displayable singular label of the resource.
|
||||||
|
*/
|
||||||
|
public static function singularLabel(): string
|
||||||
|
{
|
||||||
|
return 'Session';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fields displayed by the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Fields\Field|\Laravel\Nova\Panel|\Laravel\Nova\ResourceTool|\Illuminate\Http\Resources\MergeValue>
|
||||||
|
*/
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
BelongsTo::make('User', 'user', User::class)
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('required')
|
||||||
|
->help('The person who started this questionnaire session.'),
|
||||||
|
|
||||||
|
BelongsTo::make('Category', 'category', CategoryResource::class)
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('required')
|
||||||
|
->help('The assessment category for this session, such as Audit or Tax.'),
|
||||||
|
|
||||||
|
BelongsTo::make('Screening', 'screening', ScreeningResource::class)
|
||||||
|
->nullable()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('nullable')
|
||||||
|
->help('The pre-screening that was completed before starting this session.'),
|
||||||
|
|
||||||
|
Select::make('Status')
|
||||||
|
->options([
|
||||||
|
'in_progress' => 'In Progress',
|
||||||
|
'completed' => 'Completed',
|
||||||
|
'abandoned' => 'Abandoned',
|
||||||
|
])
|
||||||
|
->displayUsingLabels()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('The current state of this session. "In Progress" means the user has not yet submitted, "Completed" means submitted, "Abandoned" means the user left without finishing.'),
|
||||||
|
|
||||||
|
Number::make('Score')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->rules('nullable', 'integer')
|
||||||
|
->help('The total score from all scored questions. Only "Yes" answers count as points.'),
|
||||||
|
|
||||||
|
Select::make('Result')
|
||||||
|
->options([
|
||||||
|
'go' => 'Go',
|
||||||
|
'no_go' => 'No Go',
|
||||||
|
'consult_leadership' => 'Consult Leadership',
|
||||||
|
])
|
||||||
|
->displayUsingLabels()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('The final outcome based on the score. "Go" (10+ points) means pursue the opportunity, "Consult Leadership" (5-9 points) means seek advice, "No Go" (1-4 points) means do not pursue.'),
|
||||||
|
|
||||||
|
Textarea::make('Additional Comments')
|
||||||
|
->rules('nullable')
|
||||||
|
->help('Any extra notes the user added at the end of the questionnaire.'),
|
||||||
|
|
||||||
|
DateTime::make('Completed At')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->rules('nullable')
|
||||||
|
->help('The date and time when the user submitted this session.'),
|
||||||
|
|
||||||
|
HasMany::make('Answers', 'answers', AnswerResource::class),
|
||||||
|
|
||||||
|
HasMany::make('Logs', 'logs', LogResource::class),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cards available for the request.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Filters\Filter>
|
||||||
|
*/
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lenses available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Lenses\Lens>
|
||||||
|
*/
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actions available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Actions\Action>
|
||||||
|
*/
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new DownloadExcel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
143
app/Nova/User.php
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Laravel\Nova\Auth\PasswordValidationRules;
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Password;
|
||||||
|
use Laravel\Nova\Fields\Text;
|
||||||
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
|
||||||
|
final class User extends Resource
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model the resource corresponds to.
|
||||||
|
*
|
||||||
|
* @var class-string<\App\Models\User>
|
||||||
|
*/
|
||||||
|
public static $model = \App\Models\User::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single value that should be used to represent the resource when being displayed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static $title = 'name';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns that should be searched.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $search = [
|
||||||
|
'id', 'name', 'email', 'department', 'job_title',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fields displayed by the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Fields\Field|\Laravel\Nova\Panel|\Laravel\Nova\ResourceTool|\Illuminate\Http\Resources\MergeValue>
|
||||||
|
*/
|
||||||
|
public function fields(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
BelongsTo::make('Role', 'role', RoleResource::class)
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('The user\'s role, which controls what they can access in the admin panel.'),
|
||||||
|
|
||||||
|
Text::make('Name')
|
||||||
|
->sortable()
|
||||||
|
->rules('required', 'max:255')
|
||||||
|
->help('The user\'s full name, imported from Azure AD when they first log in.'),
|
||||||
|
|
||||||
|
Text::make('Email')
|
||||||
|
->sortable()
|
||||||
|
->rules('required', 'email', 'max:254')
|
||||||
|
->creationRules('unique:users,email')
|
||||||
|
->updateRules('unique:users,email,{{resourceId}}')
|
||||||
|
->help('The user\'s email address, used to identify them when logging in via Azure AD.'),
|
||||||
|
|
||||||
|
Text::make('Azure ID', 'azure_id')
|
||||||
|
->onlyOnDetail()
|
||||||
|
->copyable()
|
||||||
|
->help('A unique identifier from Azure AD. Set automatically when the user logs in.'),
|
||||||
|
|
||||||
|
Text::make('Photo', 'photo')
|
||||||
|
->onlyOnDetail()
|
||||||
|
->copyable()
|
||||||
|
->help('A link to the user\'s profile photo from Azure AD.'),
|
||||||
|
|
||||||
|
Text::make('Job Title', 'job_title')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->help('The user\'s job title, imported from Azure AD.'),
|
||||||
|
|
||||||
|
Text::make('Department')
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->copyable()
|
||||||
|
->help('The department the user belongs to, imported from Azure AD.'),
|
||||||
|
|
||||||
|
Text::make('Phone')
|
||||||
|
->sortable()
|
||||||
|
->copyable()
|
||||||
|
->help('The user\'s phone number, imported from Azure AD.'),
|
||||||
|
|
||||||
|
Password::make('Password')
|
||||||
|
->onlyOnForms()
|
||||||
|
->creationRules($this->passwordRules())
|
||||||
|
->updateRules($this->optionalPasswordRules())
|
||||||
|
->help('Only needed for admin panel access. Regular users log in via Azure AD and do not need a password.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cards available for the request.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Card>
|
||||||
|
*/
|
||||||
|
public function cards(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Filters\Filter>
|
||||||
|
*/
|
||||||
|
public function filters(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lenses available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Lenses\Lens>
|
||||||
|
*/
|
||||||
|
public function lenses(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actions available for the resource.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Actions\Action>
|
||||||
|
*/
|
||||||
|
public function actions(NovaRequest $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Policies/AnswerPolicy.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Answer;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final class AnswerPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any answers.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the answer.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Answer $answer): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create answers.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the answer.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Answer $answer): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the answer.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Answer $answer): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the answer.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Answer $answer): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the answer.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Answer $answer): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Policies/CategoryPolicy.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final class CategoryPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any categories.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the category.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Category $category): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create categories.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the category.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Category $category): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the category.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Category $category): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the category.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Category $category): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the category.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Category $category): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Policies/LogPolicy.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Log;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final class LogPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any logs.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the log.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Log $log): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create logs.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the log.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Log $log): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the log.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Log $log): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the log.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Log $log): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the log.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Log $log): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Policies/QuestionGroupPolicy.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\QuestionGroup;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final class QuestionGroupPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any question groups.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the question group.
|
||||||
|
*/
|
||||||
|
public function view(User $user, QuestionGroup $questionGroup): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create question groups.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the question group.
|
||||||
|
*/
|
||||||
|
public function update(User $user, QuestionGroup $questionGroup): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the question group.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, QuestionGroup $questionGroup): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the question group.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, QuestionGroup $questionGroup): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the question group.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, QuestionGroup $questionGroup): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Policies/QuestionPolicy.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Question;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final class QuestionPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any questions.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the question.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Question $question): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create questions.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the question.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Question $question): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the question.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Question $question): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the question.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Question $question): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the question.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Question $question): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Policies/ScreeningPolicy.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Screening;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final class ScreeningPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any screenings.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the screening.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Screening $screening): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create screenings.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the screening.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Screening $screening): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the screening.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Screening $screening): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the screening.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Screening $screening): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the screening.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Screening $screening): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Policies/SessionPolicy.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Session;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final class SessionPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any sessions.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the session.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Session $session): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create sessions.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the session.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Session $session): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the session.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Session $session): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the session.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Session $session): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the session.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Session $session): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use SocialiteProviders\Azure\AzureExtendSocialite;
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
|
|
||||||
|
final class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
* Registers the Microsoft Azure Socialite provider for SSO authentication.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Event::listen(SocialiteWasCalled::class, AzureExtendSocialite::class.'@handle');
|
||||||
|
}
|
||||||
|
}
|
||||||
122
app/Providers/NovaServiceProvider.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Nova\CategoryResource;
|
||||||
|
use App\Nova\Dashboards\Main;
|
||||||
|
use App\Nova\LogResource;
|
||||||
|
use App\Nova\QuestionGroupResource;
|
||||||
|
use App\Nova\QuestionResource;
|
||||||
|
use App\Nova\ScreeningResource;
|
||||||
|
use App\Nova\SessionResource;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
use Laravel\Nova\Menu\MenuItem;
|
||||||
|
use Laravel\Nova\Menu\MenuSection;
|
||||||
|
use Laravel\Nova\Nova;
|
||||||
|
use Laravel\Nova\NovaApplicationServiceProvider;
|
||||||
|
|
||||||
|
class NovaServiceProvider extends NovaApplicationServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
Nova::mainMenu(function (Request $request) {
|
||||||
|
return [
|
||||||
|
MenuSection::dashboard(Main::class)->icon('home'),
|
||||||
|
|
||||||
|
MenuSection::make('Questionnaire', [
|
||||||
|
MenuItem::resource(QuestionResource::class),
|
||||||
|
MenuItem::resource(QuestionGroupResource::class),
|
||||||
|
MenuItem::resource(CategoryResource::class),
|
||||||
|
MenuItem::resource(SessionResource::class),
|
||||||
|
MenuItem::resource(ScreeningResource::class),
|
||||||
|
])->icon('clipboard-document-list')->collapsible(),
|
||||||
|
|
||||||
|
MenuSection::make('Logs', [
|
||||||
|
MenuItem::resource(LogResource::class),
|
||||||
|
])->icon('chart-bar')->collapsible(),
|
||||||
|
|
||||||
|
MenuSection::make('Users', [
|
||||||
|
MenuItem::resource(\App\Nova\User::class),
|
||||||
|
])->icon('users')->collapsible(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the configurations for Laravel Fortify.
|
||||||
|
*/
|
||||||
|
protected function fortify(): void
|
||||||
|
{
|
||||||
|
Nova::fortify()
|
||||||
|
->features([
|
||||||
|
Features::updatePasswords(),
|
||||||
|
Features::emailVerification(),
|
||||||
|
Features::twoFactorAuthentication(['confirm' => true, 'confirmPassword' => true]),
|
||||||
|
])
|
||||||
|
->register();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the Nova routes.
|
||||||
|
*/
|
||||||
|
protected function routes(): void
|
||||||
|
{
|
||||||
|
Nova::routes()
|
||||||
|
->withAuthenticationRoutes(default: false)
|
||||||
|
->withPasswordResetRoutes()
|
||||||
|
->withEmailVerificationRoutes()
|
||||||
|
->register();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the Nova gate.
|
||||||
|
*
|
||||||
|
* This gate determines who can access Nova in non-local environments.
|
||||||
|
*/
|
||||||
|
protected function gate(): void
|
||||||
|
{
|
||||||
|
Gate::define('viewNova', function (User $user) {
|
||||||
|
return $user->role?->name === 'admin';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dashboards that should be listed in the Nova sidebar.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Dashboard>
|
||||||
|
*/
|
||||||
|
protected function dashboards(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new \App\Nova\Dashboards\Main,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tools that should be listed in the Nova sidebar.
|
||||||
|
*
|
||||||
|
* @return array<int, \Laravel\Nova\Tool>
|
||||||
|
*/
|
||||||
|
public function tools(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
parent::register();
|
||||||
|
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Services/ActivityLogger.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Log;
|
||||||
|
|
||||||
|
final class ActivityLogger
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Log an activity to the database.
|
||||||
|
*/
|
||||||
|
public static function log(
|
||||||
|
string $action,
|
||||||
|
?int $userId = null,
|
||||||
|
?int $sessionId = null,
|
||||||
|
?int $categoryId = null,
|
||||||
|
?array $metadata = null,
|
||||||
|
): void {
|
||||||
|
Log::create([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'session_id' => $sessionId,
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'action' => $action,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Services/ScoringService.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Session;
|
||||||
|
|
||||||
|
final class ScoringService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calculate the score for a session based on scored answers.
|
||||||
|
*/
|
||||||
|
public function calculateScore(Session $session): int
|
||||||
|
{
|
||||||
|
return $session->answers()
|
||||||
|
->whereHas('question', fn ($q) => $q->where('is_scored', true))
|
||||||
|
->where('value', 'yes')
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the result based on the score.
|
||||||
|
*/
|
||||||
|
public function determineResult(int $score): string
|
||||||
|
{
|
||||||
|
if ($score >= 10) {
|
||||||
|
return 'go';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($score >= 5) {
|
||||||
|
return 'consult_leadership';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'no_go';
|
||||||
|
}
|
||||||
|
}
|
||||||
18
artisan
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
40
bootstrap/app.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->web(append: [
|
||||||
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$middleware->alias([
|
||||||
|
'encrypt.history' => \Inertia\Middleware\EncryptHistory::class,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
$exceptions->respond(function (\Symfony\Component\HttpFoundation\Response $response, \Throwable $exception, \Illuminate\Http\Request $request) {
|
||||||
|
if (! app()->environment('local') && in_array($response->getStatusCode(), [403, 404, 500, 503])) {
|
||||||
|
return \Inertia\Inertia::render('ErrorPage', ['status' => $response->getStatusCode()])
|
||||||
|
->toResponse($request)
|
||||||
|
->setStatusCode($response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->getStatusCode() === 419) {
|
||||||
|
return back()->with([
|
||||||
|
'message' => 'The page expired, please try again.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
});
|
||||||
|
})->create();
|
||||||
2
bootstrap/cache/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
6
bootstrap/providers.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\NovaServiceProvider::class,
|
||||||
|
];
|
||||||
98
composer.json
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/laravel",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": ["laravel", "framework"],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.4",
|
||||||
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/nova": "^5.0",
|
||||||
|
"laravel/socialite": "^5.24",
|
||||||
|
"laravel/tinker": "^2.10.1",
|
||||||
|
"maatwebsite/laravel-nova-excel": "^1.3",
|
||||||
|
"socialiteproviders/microsoft-azure": "^5.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/nova-devtool": "^1.8",
|
||||||
|
"laravel/pail": "^1.2.2",
|
||||||
|
"laravel/pint": "^1.24",
|
||||||
|
"laravel/sail": "^1.41",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpunit/phpunit": "^11.5.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "composer",
|
||||||
|
"url": "https://nova.laravel.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10936
composer.lock
generated
Normal file
126
config/app.php
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
115
config/auth.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the number of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
117
config/cache.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "octane",
|
||||||
|
| "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'stores' => [
|
||||||
|
'database',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
|
];
|
||||||
183
config/database.php
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
80
config/filesystems.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
159
config/fortify.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Guard
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which authentication guard Fortify will use while
|
||||||
|
| authenticating users. This value should correspond with one of your
|
||||||
|
| guards that is already present in your "auth" configuration file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => 'web',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Password Broker
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which password broker Fortify can use when a user
|
||||||
|
| is resetting their password. This configured value should match one
|
||||||
|
| of your password brokers setup in your "auth" configuration file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => 'users',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Username / Email
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value defines which model attribute should be considered as your
|
||||||
|
| application's "username" field. Typically, this might be the email
|
||||||
|
| address of the users but you are free to change this value here.
|
||||||
|
|
|
||||||
|
| Out of the box, Fortify expects forgot password and reset password
|
||||||
|
| requests to have a field named 'email'. If the application uses
|
||||||
|
| another name for the field you may define it below as needed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'username' => 'email',
|
||||||
|
|
||||||
|
'email' => 'email',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Lowercase Usernames
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value defines whether usernames should be lowercased before saving
|
||||||
|
| them in the database, as some database system string fields are case
|
||||||
|
| sensitive. You may disable this for your application if necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lowercase_usernames' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Home Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the path where users will get redirected during
|
||||||
|
| authentication or password reset when the operations are successful
|
||||||
|
| and the user is authenticated. You are free to change this value.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'home' => '/home',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Routes Prefix / Subdomain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which prefix Fortify will assign to all the routes
|
||||||
|
| that it registers with the application. If necessary, you may change
|
||||||
|
| subdomain under which all of the Fortify routes will be available.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => '',
|
||||||
|
|
||||||
|
'domain' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Routes Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which middleware Fortify will assign to the routes
|
||||||
|
| that it registers with the application. If necessary, you may change
|
||||||
|
| these middleware but typically this provided default is preferred.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Rate Limiting
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By default, Fortify will throttle logins to five requests per minute for
|
||||||
|
| every email and IP address combination. However, if you would like to
|
||||||
|
| specify a custom rate limiter to call then you may specify it here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'limiters' => [
|
||||||
|
'login' => 'login',
|
||||||
|
'two-factor' => 'two-factor',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register View Routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify if the routes returning views should be disabled as
|
||||||
|
| you may not need them when building your own application. This may be
|
||||||
|
| especially true if you're writing a custom single-page application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'views' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Features
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some of the Fortify features are optional. You may disable the features
|
||||||
|
| by removing them from this array. You're free to only remove some of
|
||||||
|
| these features or you can even remove all of these if you need to.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'features' => [
|
||||||
|
Features::registration(),
|
||||||
|
Features::resetPasswords(),
|
||||||
|
// Features::emailVerification(),
|
||||||
|
Features::updateProfileInformation(),
|
||||||
|
Features::updatePasswords(),
|
||||||
|
Features::twoFactorAuthentication([
|
||||||
|
'confirm' => true,
|
||||||
|
'confirmPassword' => true,
|
||||||
|
// 'window' => 0,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
132
config/logging.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
118
config/mail.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
205
config/nova.php
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova License Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following configuration option contains your Nova license key. On
|
||||||
|
| non-local domains, Nova will verify that the Nova installation has
|
||||||
|
| a valid license associated with the application's active domain.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'license_key' => env('NOVA_LICENSE_KEY'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova App Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application. This value is used when the
|
||||||
|
| framework needs to display the name of the application within the UI
|
||||||
|
| or in other locations. Of course, you're free to change the value.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('NOVA_APP_NAME', env('APP_NAME')),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Domain Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the "domain name" associated with your application. This
|
||||||
|
| can be used to prevent Nova's internal routes from being registered
|
||||||
|
| on subdomains which do not need access to your admin application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('NOVA_DOMAIN_NAME', null),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the URI path where Nova will be accessible from. Feel free to
|
||||||
|
| change this path to anything you like. Note that this URI will not
|
||||||
|
| affect Nova's internal API routes which aren't exposed to users.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => '/cp',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Authentication Guard
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration option defines the authentication guard that will
|
||||||
|
| be used to protect your Nova routes. This option should match one
|
||||||
|
| of the authentication guards defined in the "auth" config file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => env('NOVA_GUARD', null),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Password Reset Broker
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration option defines the password broker that will be
|
||||||
|
| used when passwords are reset. This option should mirror one of
|
||||||
|
| the password reset options defined in the "auth" config file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => env('NOVA_PASSWORDS', null),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Route Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These middleware will be assigned to every Nova route, giving you the
|
||||||
|
| chance to add your own middleware to this stack or override any of
|
||||||
|
| the existing middleware. Or, you can just stick with this stack.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => [
|
||||||
|
'web',
|
||||||
|
\Laravel\Nova\Http\Middleware\HandleInertiaRequests::class,
|
||||||
|
'nova:serving',
|
||||||
|
],
|
||||||
|
|
||||||
|
'api_middleware' => [
|
||||||
|
'nova',
|
||||||
|
\Laravel\Nova\Http\Middleware\Authenticate::class,
|
||||||
|
// \Laravel\Nova\Http\Middleware\AuthenticateSession::class,
|
||||||
|
\Laravel\Nova\Http\Middleware\EnsureEmailIsVerified::class,
|
||||||
|
\Laravel\Nova\Http\Middleware\Authorize::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'asset_middleware' => [
|
||||||
|
'nova:api',
|
||||||
|
\Illuminate\Http\Middleware\CheckResponseForModifications::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Pagination Type
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the visual style used in Nova's resource pagination
|
||||||
|
| views. You may select between "simple", "load-more", and "links" for
|
||||||
|
| your applications. Feel free to adjust this option to your choice.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'pagination' => 'simple',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Storage Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration option allows you to define the default disk that
|
||||||
|
| will be used to store files using the Image, File, and other file
|
||||||
|
| related field types. You're welcome to use any configured disk.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'storage_disk' => env('NOVA_STORAGE_DISK', 'public'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Currency
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration option allows you to define the default currency
|
||||||
|
| used by the Currency field within Nova. You may change this to a
|
||||||
|
| valid ISO 4217 currency code to suit your application's needs.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'currency' => 'USD',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Branding
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration values allow you to customize the branding of the
|
||||||
|
| Nova interface, including the primary color and the logo that will
|
||||||
|
| be displayed within the Nova interface. This logo value must be
|
||||||
|
| the absolute path to an SVG logo within the local filesystem.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 'brand' => [
|
||||||
|
// 'logo' => resource_path('/img/example-logo.svg'),
|
||||||
|
|
||||||
|
// 'colors' => [
|
||||||
|
// "400" => "24, 182, 155, 0.5",
|
||||||
|
// "500" => "24, 182, 155",
|
||||||
|
// "600" => "24, 182, 155, 0.75",
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Action Resource Class
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration option allows you to specify a custom resource class
|
||||||
|
| to use for action log entries instead of the default that ships with
|
||||||
|
| Nova, thus allowing for the addition of additional UI form fields.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'actions' => [
|
||||||
|
'resource' => \Laravel\Nova\Actions\ActionResource::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Nova Impersonation Redirection URLs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration option allows you to specify a URL where Nova should
|
||||||
|
| redirect an administrator after impersonating another user and a URL
|
||||||
|
| to redirect the administrator after stopping impersonating a user.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'impersonation' => [
|
||||||
|
'started' => '/',
|
||||||
|
'stopped' => '/',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
129
config/queue.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the connection options for every queue backend
|
||||||
|
| used by your application. An example configuration is provided for
|
||||||
|
| each backend supported by Laravel. You're also free to add more.
|
||||||
|
|
|
||||||
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||||
|
| "deferred", "background", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sync' => [
|
||||||
|
'driver' => 'sync',
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||||
|
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||||
|
'queue' => env('DB_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'beanstalkd' => [
|
||||||
|
'driver' => 'beanstalkd',
|
||||||
|
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||||
|
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => 0,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqs' => [
|
||||||
|
'driver' => 'sqs',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||||
|
'queue' => env('SQS_QUEUE', 'default'),
|
||||||
|
'suffix' => env('SQS_SUFFIX'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => null,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'deferred' => [
|
||||||
|
'driver' => 'deferred',
|
||||||
|
],
|
||||||
|
|
||||||
|
'background' => [
|
||||||
|
'driver' => 'background',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'connections' => [
|
||||||
|
'database',
|
||||||
|
'deferred',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Batching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following options configure the database and table that store job
|
||||||
|
| batching information. These options can be updated to any database
|
||||||
|
| connection and table which has been defined by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'batching' => [
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'job_batches',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Failed Queue Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure the behavior of failed queue job logging so you
|
||||||
|
| can control how and where failed jobs are stored. Laravel ships with
|
||||||
|
| support for storing failed jobs in a simple file or in a database.
|
||||||
|
|
|
||||||
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'failed' => [
|
||||||
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'failed_jobs',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
42
config/screening.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Pre-Screening Questions
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These 10 Yes/No questions are presented before category selection.
|
||||||
|
| Each "Yes" answer scores 1 point. A score of 5 or more is required
|
||||||
|
| to proceed to the category questionnaire.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'questions' => [
|
||||||
|
1 => 'Is the opportunity aligned with our strategic goals?',
|
||||||
|
2 => 'Do we have the necessary expertise to deliver?',
|
||||||
|
3 => 'Is the client financially stable?',
|
||||||
|
4 => 'Are there no significant conflicts of interest?',
|
||||||
|
5 => 'Is the timeline realistic?',
|
||||||
|
6 => 'Do we have available resources?',
|
||||||
|
7 => 'Is the expected fee reasonable for the scope?',
|
||||||
|
8 => 'Are the client\'s expectations manageable?',
|
||||||
|
9 => 'Have we successfully completed similar engagements?',
|
||||||
|
10 => 'Is the risk level acceptable?',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Passing Score Threshold
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Minimum score required to proceed to category selection.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passing_score' => 5,
|
||||||
|
|
||||||
|
];
|
||||||
45
config/services.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'key' => env('RESEND_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'notifications' => [
|
||||||
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'azure' => [
|
||||||
|
'client_id' => env('AZURE_CLIENT_ID'),
|
||||||
|
'client_secret' => env('AZURE_CLIENT_SECRET'),
|
||||||
|
'redirect' => env('AZURE_REDIRECT_URI', '/auth/callback'),
|
||||||
|
'tenant' => env('AZURE_TENANT_ID', 'common'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
217
config/session.php
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Lifetime
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the number of minutes that you wish the session
|
||||||
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
|
| to expire immediately when the browser is closed then you may
|
||||||
|
| indicate that via the expire_on_close configuration option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Encryption
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to easily specify that all of your session data
|
||||||
|
| should be encrypted before it's stored. All encryption is performed
|
||||||
|
| automatically by Laravel and you may use the session like normal.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session File Location
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the "file" session driver, the session files are placed
|
||||||
|
| on disk. The default storage location is defined here; however, you
|
||||||
|
| are free to provide another location where they should be stored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" or "redis" session drivers, you may specify a
|
||||||
|
| connection that should be used to manage these sessions. This should
|
||||||
|
| correspond to a connection in your database configuration options.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connection' => env('SESSION_CONNECTION'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" session driver, you may specify the table to
|
||||||
|
| be used to store sessions. Of course, a sensible default is defined
|
||||||
|
| for you; however, you're welcome to change this to another table.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using one of the framework's cache driven session backends, you may
|
||||||
|
| define the cache store which should be used to store the session data
|
||||||
|
| between requests. This must match one of your defined cache stores.
|
||||||
|
|
|
||||||
|
| Affects: "dynamodb", "memcached", "redis"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Sweeping Lottery
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some session drivers must manually sweep their storage location to get
|
||||||
|
| rid of old sessions from storage. Here are the chances that it will
|
||||||
|
| happen on a given request. By default, the odds are 2 out of 100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may change the name of the session cookie that is created by
|
||||||
|
| the framework. Typically, you should not need to change this value
|
||||||
|
| since doing so does not grant a meaningful security improvement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cookie' => env(
|
||||||
|
'SESSION_COOKIE',
|
||||||
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The session cookie path determines the path for which the cookie will
|
||||||
|
| be regarded as available. Typically, this will be the root path of
|
||||||
|
| your application, but you're free to change this when necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the domain and subdomains the session cookie is
|
||||||
|
| available to. By default, the cookie will be available to the root
|
||||||
|
| domain without subdomains. Typically, this shouldn't be changed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTPS Only Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By setting this option to true, session cookies will only be sent back
|
||||||
|
| to the server if the browser has a HTTPS connection. This will keep
|
||||||
|
| the cookie from being sent to you when it can't be done securely.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTP Access Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
|
| value of the cookie and the cookie will only be accessible through
|
||||||
|
| the HTTP protocol. It's unlikely you should disable this option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Same-Site Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines how your cookies behave when cross-site requests
|
||||||
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
|
| will set this value to "lax" to permit secure cross-site requests.
|
||||||
|
|
|
||||||
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||||
|
|
|
||||||
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Partitioned Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will tie the cookie to the top-level site for
|
||||||
|
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||||
|
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
|
];
|
||||||
12
cypress.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'https://go-no-go.test',
|
||||||
|
supportFile: 'cypress/support/e2e.js',
|
||||||
|
specPattern: 'cypress/e2e/**/*.cy.{js,jsx}',
|
||||||
|
viewportWidth: 1280,
|
||||||
|
viewportHeight: 720,
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
},
|
||||||
|
})
|
||||||
43
cypress/e2e/questionnaire-flow.cy.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
describe('Questionnaire Flow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetDatabase()
|
||||||
|
cy.login()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('completes the full questionnaire flow from landing to result', () => {
|
||||||
|
// 1. Landing page — click Continue
|
||||||
|
cy.get('[data-cy="start-screening"]').click()
|
||||||
|
|
||||||
|
// 2. Screening — answer all 10 questions with Yes
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
cy.get(`[data-cy="screening-answer-${i}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="yes"]').check({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cy.get('[data-cy="submit-screening"]').click()
|
||||||
|
|
||||||
|
// 3. Screening result — should pass with 10/10
|
||||||
|
cy.get('[data-cy="result-passed"]').should('exist')
|
||||||
|
cy.get('[data-cy="screening-score"]').should('contain', '10')
|
||||||
|
|
||||||
|
// 4. Select first category (Audit)
|
||||||
|
cy.get('[data-cy="category-select"]').within(() => {
|
||||||
|
cy.contains('button', 'Start').first().click()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. Session/Show — should see questionnaire
|
||||||
|
cy.url().should('include', '/sessions/')
|
||||||
|
cy.contains('Questionnaire').should('be.visible')
|
||||||
|
|
||||||
|
// 6. Complete session
|
||||||
|
cy.get('[data-cy="complete-session"]').click()
|
||||||
|
|
||||||
|
// 7. Session result page
|
||||||
|
cy.url().should('include', '/result')
|
||||||
|
cy.get('[data-cy="session-result"]').should('exist')
|
||||||
|
|
||||||
|
// 8. Click Again to go back
|
||||||
|
cy.get('[data-cy="start-new"]').click()
|
||||||
|
cy.url().should('eq', Cypress.config('baseUrl') + '/')
|
||||||
|
})
|
||||||
|
})
|
||||||
54
cypress/e2e/result-page.cy.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
describe('Result Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetDatabase()
|
||||||
|
cy.login()
|
||||||
|
})
|
||||||
|
|
||||||
|
function passScreeningAndStartSession() {
|
||||||
|
cy.get('[data-cy="start-screening"]').click()
|
||||||
|
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
cy.get(`[data-cy="screening-answer-${i}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="yes"]').check({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cy.get('[data-cy="submit-screening"]').click()
|
||||||
|
|
||||||
|
cy.get('[data-cy="category-select"]').within(() => {
|
||||||
|
cy.contains('button', 'Start').first().click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.url().should('include', '/sessions/')
|
||||||
|
}
|
||||||
|
|
||||||
|
it('shows session result after completion', () => {
|
||||||
|
passScreeningAndStartSession()
|
||||||
|
|
||||||
|
// Just complete without answering specific questions
|
||||||
|
cy.get('[data-cy="complete-session"]').click()
|
||||||
|
|
||||||
|
cy.url().should('include', '/result')
|
||||||
|
cy.get('[data-cy="session-result"]').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the result badge with correct result type', () => {
|
||||||
|
passScreeningAndStartSession()
|
||||||
|
|
||||||
|
cy.get('[data-cy="complete-session"]').click()
|
||||||
|
|
||||||
|
cy.url().should('include', '/result')
|
||||||
|
// Should show one of the result types
|
||||||
|
cy.get('[data-cy="session-result"]').should('exist')
|
||||||
|
cy.get('[data-cy^="result-"]').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can start over from result page', () => {
|
||||||
|
passScreeningAndStartSession()
|
||||||
|
|
||||||
|
cy.get('[data-cy="complete-session"]').click()
|
||||||
|
cy.url().should('include', '/result')
|
||||||
|
|
||||||
|
cy.get('[data-cy="start-new"]').click()
|
||||||
|
cy.url().should('eq', Cypress.config('baseUrl') + '/')
|
||||||
|
})
|
||||||
|
})
|
||||||
69
cypress/e2e/scoring-display.cy.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
describe('Scoring Display', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetDatabase()
|
||||||
|
cy.login()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows No Go result when fewer than 5 yes answers', () => {
|
||||||
|
cy.get('[data-cy="start-screening"]').click()
|
||||||
|
|
||||||
|
// Answer 4 yes, 6 no
|
||||||
|
for (let i = 1; i <= 4; i++) {
|
||||||
|
cy.get(`[data-cy="screening-answer-${i}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="yes"]').check({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (let i = 5; i <= 10; i++) {
|
||||||
|
cy.get(`[data-cy="screening-answer-${i}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="no"]').check({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cy.get('[data-cy="submit-screening"]').click()
|
||||||
|
|
||||||
|
// Should fail
|
||||||
|
cy.get('[data-cy="result-failed"]').should('exist')
|
||||||
|
cy.get('[data-cy="screening-score"]').should('contain', '4')
|
||||||
|
cy.get('[data-cy="category-select"]').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes at boundary with exactly 5 yes answers', () => {
|
||||||
|
cy.get('[data-cy="start-screening"]').click()
|
||||||
|
|
||||||
|
// Answer 5 yes, 5 no
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
cy.get(`[data-cy="screening-answer-${i}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="yes"]').check({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (let i = 6; i <= 10; i++) {
|
||||||
|
cy.get(`[data-cy="screening-answer-${i}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="no"]').check({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cy.get('[data-cy="submit-screening"]').click()
|
||||||
|
|
||||||
|
// Should pass
|
||||||
|
cy.get('[data-cy="result-passed"]').should('exist')
|
||||||
|
cy.get('[data-cy="screening-score"]').should('contain', '5')
|
||||||
|
cy.get('[data-cy="category-select"]').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays the score correctly', () => {
|
||||||
|
cy.get('[data-cy="start-screening"]').click()
|
||||||
|
|
||||||
|
// Answer 7 yes, 3 no
|
||||||
|
for (let i = 1; i <= 7; i++) {
|
||||||
|
cy.get(`[data-cy="screening-answer-${i}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="yes"]').check({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (let i = 8; i <= 10; i++) {
|
||||||
|
cy.get(`[data-cy="screening-answer-${i}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="no"]').check({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cy.get('[data-cy="submit-screening"]').click()
|
||||||
|
|
||||||
|
cy.get('[data-cy="screening-score"]').should('contain', '7')
|
||||||
|
})
|
||||||
|
})
|
||||||
8
cypress/support/commands.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Cypress.Commands.add('login', () => {
|
||||||
|
cy.visit('/login-jonathan')
|
||||||
|
cy.url().should('include', '/')
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('resetDatabase', () => {
|
||||||
|
cy.exec('herd php artisan migrate:fresh --seed --force', { timeout: 30000 })
|
||||||
|
})
|
||||||
1
cypress/support/e2e.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './commands'
|
||||||
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
||||||
33
database/factories/AnswerFactory.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Answer;
|
||||||
|
use App\Models\Question;
|
||||||
|
use App\Models\Session;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for generating Answer test data.
|
||||||
|
*
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Answer>
|
||||||
|
*/
|
||||||
|
final class AnswerFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Answer::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'session_id' => Session::factory(),
|
||||||
|
'question_id' => Question::factory(),
|
||||||
|
'value' => fake()->randomElement(['yes', 'no', 'not_applicable']),
|
||||||
|
'text_value' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
29
database/factories/CategoryFactory.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for generating Category test data.
|
||||||
|
*
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Category>
|
||||||
|
*/
|
||||||
|
final class CategoryFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Category::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->unique()->words(2, true),
|
||||||
|
'sort_order' => fake()->numberBetween(0, 10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
38
database/factories/LogFactory.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Log;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for generating Log test data.
|
||||||
|
*
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Log>
|
||||||
|
*/
|
||||||
|
final class LogFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Log::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => User::factory(),
|
||||||
|
'action' => fake()->randomElement([
|
||||||
|
'login',
|
||||||
|
'logout',
|
||||||
|
'session_started',
|
||||||
|
'session_completed',
|
||||||
|
'screening_started',
|
||||||
|
'screening_completed',
|
||||||
|
]),
|
||||||
|
'metadata' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
60
database/factories/QuestionFactory.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Question;
|
||||||
|
use App\Models\QuestionGroup;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for generating Question test data.
|
||||||
|
*
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Question>
|
||||||
|
*/
|
||||||
|
final class QuestionFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Question::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'question_group_id' => QuestionGroup::factory(),
|
||||||
|
'text' => fake()->sentence(),
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => null,
|
||||||
|
'sort_order' => fake()->numberBetween(0, 10),
|
||||||
|
'is_scored' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the question is not scored.
|
||||||
|
*/
|
||||||
|
public function nonScored(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'is_scored' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the question is text-only (no yes/no/na options).
|
||||||
|
*/
|
||||||
|
public function textOnly(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'is_scored' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
database/factories/QuestionGroupFactory.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\QuestionGroup;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for generating QuestionGroup test data.
|
||||||
|
*
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\QuestionGroup>
|
||||||
|
*/
|
||||||
|
final class QuestionGroupFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = QuestionGroup::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'category_id' => Category::factory(),
|
||||||
|
'name' => fake()->words(3, true),
|
||||||
|
'sort_order' => fake()->numberBetween(0, 10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
database/factories/ScreeningAnswerFactory.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Screening;
|
||||||
|
use App\Models\ScreeningAnswer;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for generating ScreeningAnswer test data.
|
||||||
|
*
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ScreeningAnswer>
|
||||||
|
*/
|
||||||
|
final class ScreeningAnswerFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = ScreeningAnswer::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'screening_id' => Screening::factory(),
|
||||||
|
'question_number' => fake()->numberBetween(1, 10),
|
||||||
|
'value' => fake()->randomElement(['yes', 'no']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
53
database/factories/ScreeningFactory.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Screening;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for generating Screening test data.
|
||||||
|
*
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Screening>
|
||||||
|
*/
|
||||||
|
final class ScreeningFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Screening::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => User::factory(),
|
||||||
|
'score' => null,
|
||||||
|
'passed' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the screening passed.
|
||||||
|
*/
|
||||||
|
public function passed(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'score' => 10,
|
||||||
|
'passed' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the screening failed.
|
||||||
|
*/
|
||||||
|
public function failed(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'score' => 3,
|
||||||
|
'passed' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
database/factories/SessionFactory.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\Session;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for generating Session test data.
|
||||||
|
*
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Session>
|
||||||
|
*/
|
||||||
|
final class SessionFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Session::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => User::factory(),
|
||||||
|
'category_id' => Category::factory(),
|
||||||
|
'screening_id' => null,
|
||||||
|
'status' => 'in_progress',
|
||||||
|
'score' => null,
|
||||||
|
'result' => null,
|
||||||
|
'additional_comments' => null,
|
||||||
|
'completed_at' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the session is completed.
|
||||||
|
*/
|
||||||
|
public function completed(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'status' => 'completed',
|
||||||
|
'score' => 8,
|
||||||
|
'result' => 'consult_leadership',
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||||
|
*/
|
||||||
|
final class UserFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current password being used by the factory.
|
||||||
|
*/
|
||||||
|
protected static ?string $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->name(),
|
||||||
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => self::$password ??= Hash::make('password'),
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
'job_title' => fake()->jobTitle(),
|
||||||
|
'company_name' => fake()->company(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model's email address should be unverified.
|
||||||
|
*/
|
||||||
|
public function unverified(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'email_verified_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
database/migrations/0000_00_00_000000_create_roles_table.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('roles', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->unique();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
|
||||||
|
DB::table('roles')->insert([
|
||||||
|
['name' => 'user', 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
['name' => 'admin', 'created_at' => $now, 'updated_at' => $now],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('roles');
|
||||||
|
}
|
||||||
|
};
|
||||||