Compare commits
29 Commits
e8be239c32
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ede31b15cb | |||
| aad1d8a2b2 | |||
| 3cddb1c609 | |||
| e44ef5fddc | |||
| e98ca8f00c | |||
| 0327b95568 | |||
| 9d61186c72 | |||
| 953afd02e6 | |||
| f1824ff752 | |||
| 78c51d55b5 | |||
| 514f1cb483 | |||
| 77edd1b666 | |||
| fb1c28a0ba | |||
| c39b8085af | |||
| eb43b35873 | |||
| f57bdd68da | |||
| e4b3689e64 | |||
| 84355f2463 | |||
| e4259978de | |||
| 9a10ff4727 | |||
| 4dc64c22cb | |||
| ebaeb1722d | |||
| 07a8276899 | |||
| bc1d5a2796 | |||
| baa43de4e1 | |||
| c693cde038 | |||
| 9583b7030c | |||
| cf5d988bbc | |||
| 0b6c6736ef |
@@ -24,7 +24,14 @@
|
|||||||
"Write",
|
"Write",
|
||||||
"Bash",
|
"Bash",
|
||||||
"mcp__playwright__browser_console_messages",
|
"mcp__playwright__browser_console_messages",
|
||||||
"mcp__playwright__browser_navigate_back"
|
"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",
|
||||||
|
"mcp__playwright__browser_press_key"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
.env.example
@@ -1,8 +1,8 @@
|
|||||||
APP_NAME=Laravel
|
APP_NAME="Go No Go"
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost
|
APP_URL=http://go-no-go.test
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
@@ -20,12 +20,12 @@ LOG_STACK=single
|
|||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=mysql
|
||||||
# DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
# DB_PORT=3306
|
DB_PORT=3306
|
||||||
# DB_DATABASE=laravel
|
DB_DATABASE=go-no-go
|
||||||
# DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
# DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
@@ -63,3 +63,9 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AZURE_CLIENT_ID=
|
||||||
|
AZURE_CLIENT_SECRET=
|
||||||
|
AZURE_REDIRECT_URI=/auth/callback
|
||||||
|
AZURE_TENANT_ID=common
|
||||||
|
NOVA_LICENSE_KEY=
|
||||||
|
|||||||
BIN
.playwright-mcp/element-2026-02-19T13-33-20-065Z.png
Normal file
|
After Width: | Height: | Size: 479 KiB |
BIN
.playwright-mcp/element-2026-02-19T13-37-44-734Z.png
Normal file
|
After Width: | Height: | Size: 479 KiB |
BIN
.playwright-mcp/element-2026-02-19T13-40-05-307Z.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
.playwright-mcp/element-2026-02-19T13-40-12-397Z.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
.playwright-mcp/go-no-go-1.pdf
Normal file
BIN
.playwright-mcp/page-2026-02-19T13-34-20-685Z.png
Normal file
|
After Width: | Height: | Size: 511 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]
|
||||||
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,12 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
use Laravel\Socialite\Facades\Socialite;
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
|
|
||||||
final class SocialiteController extends Controller
|
final class SocialiteController extends Controller
|
||||||
@@ -27,16 +30,27 @@ public function callback(): RedirectResponse
|
|||||||
{
|
{
|
||||||
$azureUser = Socialite::driver('azure')->user();
|
$azureUser = Socialite::driver('azure')->user();
|
||||||
|
|
||||||
$user = User::query()->firstOrCreate(
|
$user = User::query()->updateOrCreate(
|
||||||
['email' => $azureUser->getEmail()],
|
['email' => $azureUser->getEmail()],
|
||||||
[
|
[
|
||||||
'name' => $azureUser->getName(),
|
'name' => $azureUser->getName(),
|
||||||
'password' => null,
|
'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);
|
auth()->login($user);
|
||||||
|
|
||||||
|
ActivityLogger::log('login', $user->id, metadata: ['email' => $user->email, 'firm_name' => Arr::get($azureUser->user, 'companyName')]);
|
||||||
|
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +59,8 @@ public function callback(): RedirectResponse
|
|||||||
*/
|
*/
|
||||||
public function logout(Request $request): RedirectResponse
|
public function logout(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
|
ActivityLogger::log('logout', auth()->id());
|
||||||
|
|
||||||
auth()->logout();
|
auth()->logout();
|
||||||
|
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
|
|||||||
@@ -4,10 +4,13 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\Screening\UpdateScreeningRequest;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Screening;
|
use App\Models\Screening;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
|
|
||||||
@@ -22,6 +25,8 @@ public function store(Request $request): RedirectResponse
|
|||||||
'user_id' => auth()->id(),
|
'user_id' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
ActivityLogger::log('screening_started', auth()->id());
|
||||||
|
|
||||||
return redirect()->route('screening.show', $screening);
|
return redirect()->route('screening.show', $screening);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,14 +37,22 @@ public function show(Screening $screening): Response
|
|||||||
{
|
{
|
||||||
return Inertia::render('Screening/Show', [
|
return Inertia::render('Screening/Show', [
|
||||||
'screening' => $screening,
|
'screening' => $screening,
|
||||||
|
'questions' => array_values(config('screening.questions')),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save screening answers and redirect to result.
|
* Save screening answers and redirect to result.
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, Screening $screening): RedirectResponse
|
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);
|
return redirect()->route('screening.result', $screening);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +63,58 @@ public function result(Screening $screening): Response
|
|||||||
{
|
{
|
||||||
return Inertia::render('Screening/Result', [
|
return Inertia::render('Screening/Result', [
|
||||||
'screening' => $screening,
|
'screening' => $screening,
|
||||||
'categories' => Category::orderBy('sort_order')->get(['id', 'name']),
|
'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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,18 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\Session\UpdateSessionRequest;
|
||||||
use App\Models\Session;
|
use App\Models\Session;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
|
use App\Services\ScoringService;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response as InertiaResponse;
|
||||||
|
|
||||||
final class SessionController extends Controller
|
final class SessionController extends Controller
|
||||||
{
|
{
|
||||||
@@ -24,36 +31,211 @@ public function store(Request $request): RedirectResponse
|
|||||||
'status' => 'in_progress',
|
'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);
|
return redirect()->route('sessions.show', $session);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the session questionnaire with category.
|
* Display the session questionnaire with category, question groups, questions, and existing answers.
|
||||||
*/
|
*/
|
||||||
public function show(Session $session): Response
|
public function show(Session $session): InertiaResponse
|
||||||
{
|
{
|
||||||
$session->load('category');
|
$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');
|
||||||
|
|
||||||
return Inertia::render('Session/Show', [
|
return Inertia::render('Session/Show', [
|
||||||
'session' => $session,
|
'session' => $session,
|
||||||
|
'questionGroups' => $questionGroups,
|
||||||
|
'answers' => $answers,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save session answers and redirect to result.
|
* Save session basic info, answers, and additional comments.
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, Session $session): RedirectResponse
|
public function update(UpdateSessionRequest $request, Session $session): RedirectResponse
|
||||||
{
|
{
|
||||||
|
sleep(3);
|
||||||
|
|
||||||
|
$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);
|
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.
|
* Display the final session result.
|
||||||
*/
|
*/
|
||||||
public function result(Session $session): Response
|
public function result(Session $session): InertiaResponse
|
||||||
{
|
{
|
||||||
|
$session->load('category');
|
||||||
|
|
||||||
return Inertia::render('Session/Result', [
|
return Inertia::render('Session/Result', [
|
||||||
'session' => $session,
|
'session' => $session,
|
||||||
|
'score' => $session->score,
|
||||||
|
'result' => $session->result,
|
||||||
|
'categoryName' => $session->category->name,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate and download a PDF export of the completed session result.
|
||||||
|
*/
|
||||||
|
public function pdf(Session $session): Response
|
||||||
|
{
|
||||||
|
abort_unless($session->user_id === auth()->id(), 403);
|
||||||
|
abort_unless($session->status === 'completed', 403);
|
||||||
|
|
||||||
|
$session->load(['user', 'category', 'answers.question']);
|
||||||
|
|
||||||
|
$questionGroups = $session->category
|
||||||
|
->questionGroups()
|
||||||
|
->with(['questions' => fn ($q) => $q->orderBy('sort_order')])
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
ActivityLogger::log('session_pdf_downloaded', $session->user_id, sessionId: $session->id, categoryId: $session->category_id);
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('pdf.session-result', [
|
||||||
|
'session' => $session,
|
||||||
|
'questionGroups' => $questionGroups,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $pdf->download("go-no-go-{$session->id}.pdf");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Inertia\Middleware;
|
use Inertia\Middleware;
|
||||||
|
use Laravel\Nova\Nova;
|
||||||
|
|
||||||
final class HandleInertiaRequests extends Middleware
|
final class HandleInertiaRequests extends Middleware
|
||||||
{
|
{
|
||||||
@@ -32,6 +34,7 @@ public function share(Request $request): array
|
|||||||
...parent::share($request),
|
...parent::share($request),
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $this->getAuthenticatedUser(),
|
'user' => $this->getAuthenticatedUser(),
|
||||||
|
'logo_href' => $this->getLogoHref(),
|
||||||
],
|
],
|
||||||
'flash' => [
|
'flash' => [
|
||||||
'success' => fn () => Arr::get($request->session()->all(), 'success'),
|
'success' => fn () => Arr::get($request->session()->all(), 'success'),
|
||||||
@@ -55,6 +58,22 @@ private function getAuthenticatedUser(): ?array
|
|||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
'name' => $user->name,
|
'name' => $user->name,
|
||||||
'email' => $user->email,
|
'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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Jobs/CloseSessionsJob.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Session;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
|
||||||
|
final class CloseSessionsJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all in-progress sessions idle for more than 12 hours and mark them as
|
||||||
|
* 'unfinished' status, logging each closure individually via ActivityLogger.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
Session::query()
|
||||||
|
->where('status', 'in_progress')
|
||||||
|
->where('created_at', '<', now()->subHours(12))
|
||||||
|
->each(function (Session $session): void {
|
||||||
|
$session->update([
|
||||||
|
'status' => 'unfinished',
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ActivityLogger::log(
|
||||||
|
'session_auto_closed',
|
||||||
|
$session->user_id,
|
||||||
|
sessionId: $session->id,
|
||||||
|
categoryId: $session->category_id,
|
||||||
|
metadata: ['reason' => 'idle_12h'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Jobs/LogAppVersionJob.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
final class LogAppVersionJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the application version.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$version = config('app.version', 'unknown');
|
||||||
|
|
||||||
|
Log::info("Application version: {$version}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
final class Answer extends Model
|
final class Answer extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fillable attributes for mass assignment.
|
* Fillable attributes for mass assignment.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
final class Category extends Model
|
final class Category extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fillable attributes for mass assignment.
|
* Fillable attributes for mass assignment.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
final class Log extends Model
|
final class Log extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable the updated_at timestamp for append-only logs.
|
* Disable the updated_at timestamp for append-only logs.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,11 +4,15 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
final class Question extends Model
|
final class Question extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fillable attributes for mass assignment.
|
* Fillable attributes for mass assignment.
|
||||||
*/
|
*/
|
||||||
@@ -45,4 +49,12 @@ public function questionGroup(): BelongsTo
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(QuestionGroup::class);
|
return $this->belongsTo(QuestionGroup::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all answers for this question.
|
||||||
|
*/
|
||||||
|
public function answers(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Answer::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
final class QuestionGroup extends Model
|
final class QuestionGroup extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fillable attributes for mass assignment.
|
* Fillable attributes for mass assignment.
|
||||||
*/
|
*/
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,15 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
final class Screening extends Model
|
final class Screening extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fillable attributes for mass assignment.
|
* Fillable attributes for mass assignment.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
final class ScreeningAnswer extends Model
|
final class ScreeningAnswer extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fillable attributes for mass assignment.
|
* Fillable attributes for mass assignment.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
final class Session extends Model
|
final class Session extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'questionnaire_sessions';
|
protected $table = 'questionnaire_sessions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,7 +25,6 @@ final class Session extends Model
|
|||||||
'status',
|
'status',
|
||||||
'score',
|
'score',
|
||||||
'result',
|
'result',
|
||||||
'basic_info',
|
|
||||||
'additional_comments',
|
'additional_comments',
|
||||||
'completed_at',
|
'completed_at',
|
||||||
];
|
];
|
||||||
@@ -37,7 +39,6 @@ protected function casts(): array
|
|||||||
'category_id' => 'integer',
|
'category_id' => 'integer',
|
||||||
'screening_id' => 'integer',
|
'screening_id' => 'integer',
|
||||||
'score' => 'integer',
|
'score' => 'integer',
|
||||||
'basic_info' => 'array',
|
|
||||||
'completed_at' => 'datetime',
|
'completed_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
@@ -23,6 +24,13 @@ final class User extends Authenticatable
|
|||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
|
'azure_id',
|
||||||
|
'photo',
|
||||||
|
'job_title',
|
||||||
|
'department',
|
||||||
|
'company_name',
|
||||||
|
'phone',
|
||||||
|
'role_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +41,7 @@ final class User extends Authenticatable
|
|||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'password',
|
||||||
'remember_token',
|
'remember_token',
|
||||||
|
'azure_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,9 +54,18 @@ protected function casts(): array
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'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.
|
* Get all sessions for this user.
|
||||||
*/
|
*/
|
||||||
|
|||||||
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Nova\Dashboards;
|
namespace App\Nova\Dashboards;
|
||||||
|
|
||||||
use Laravel\Nova\Cards\Help;
|
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;
|
use Laravel\Nova\Dashboards\Main as Dashboard;
|
||||||
|
|
||||||
class Main extends Dashboard
|
final class Main extends Dashboard
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the cards for the dashboard.
|
* Get the cards for the dashboard.
|
||||||
@@ -15,7 +20,10 @@ class Main extends Dashboard
|
|||||||
public function cards(): array
|
public function cards(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new Help,
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
180
app/Nova/QuestionResource.php
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
|
use Laravel\Nova\Fields\Boolean;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,17 @@
|
|||||||
|
|
||||||
abstract class Resource extends NovaResource
|
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.
|
* Build an "index" query for the given resource.
|
||||||
*/
|
*/
|
||||||
|
|||||||
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
193
app/Nova/SessionResource.php
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use App\Nova\Actions\DownloadExcel;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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',
|
||||||
|
'unfinished' => 'Unfinished',
|
||||||
|
'abandoned' => 'Abandoned',
|
||||||
|
])
|
||||||
|
->displayUsingLabels()
|
||||||
|
->sortable()
|
||||||
|
->filterable()
|
||||||
|
->help('The current state of this session. "In Progress" means the user has not yet submitted, "Completed" means submitted, "Unfinished" means the session was auto-closed after inactivity, "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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Nova;
|
namespace App\Nova;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Laravel\Nova\Auth\PasswordValidationRules;
|
use Laravel\Nova\Auth\PasswordValidationRules;
|
||||||
|
use Laravel\Nova\Fields\BelongsTo;
|
||||||
use Laravel\Nova\Fields\ID;
|
use Laravel\Nova\Fields\ID;
|
||||||
use Laravel\Nova\Fields\Password;
|
use Laravel\Nova\Fields\Password;
|
||||||
use Laravel\Nova\Fields\Text;
|
use Laravel\Nova\Fields\Text;
|
||||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||||
|
|
||||||
class User extends Resource
|
final class User extends Resource
|
||||||
{
|
{
|
||||||
use PasswordValidationRules;
|
use PasswordValidationRules;
|
||||||
|
|
||||||
@@ -33,7 +36,7 @@ class User extends Resource
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
public static $search = [
|
public static $search = [
|
||||||
'id', 'name', 'email',
|
'id', 'name', 'email', 'department', 'job_title',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,20 +49,55 @@ public function fields(NovaRequest $request): array
|
|||||||
return [
|
return [
|
||||||
ID::make()->sortable(),
|
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')
|
Text::make('Name')
|
||||||
->sortable()
|
->sortable()
|
||||||
->rules('required', 'max:255'),
|
->rules('required', 'max:255')
|
||||||
|
->help('The user\'s full name, imported from Azure AD when they first log in.'),
|
||||||
|
|
||||||
Text::make('Email')
|
Text::make('Email')
|
||||||
->sortable()
|
->sortable()
|
||||||
->rules('required', 'email', 'max:254')
|
->rules('required', 'email', 'max:254')
|
||||||
->creationRules('unique:users,email')
|
->creationRules('unique:users,email')
|
||||||
->updateRules('unique:users,email,{{resourceId}}'),
|
->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')
|
Password::make('Password')
|
||||||
->onlyOnForms()
|
->onlyOnForms()
|
||||||
->creationRules($this->passwordRules())
|
->creationRules($this->passwordRules())
|
||||||
->updateRules($this->optionalPasswordRules()),
|
->updateRules($this->optionalPasswordRules())
|
||||||
|
->help('Only needed for admin panel access. Regular users log in via Azure AD and do not need a password.'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use SocialiteProviders\Azure\AzureExtendSocialite;
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
final class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Register any application services.
|
* Register any application services.
|
||||||
@@ -16,9 +21,10 @@ public function register(): void
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Bootstrap any application services.
|
* Bootstrap any application services.
|
||||||
|
* Registers the Microsoft Azure Socialite provider for SSO authentication.
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
Event::listen(SocialiteWasCalled::class, AzureExtendSocialite::class.'@handle');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,18 @@
|
|||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Models\User;
|
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 Illuminate\Support\Facades\Gate;
|
||||||
use Laravel\Fortify\Features;
|
use Laravel\Fortify\Features;
|
||||||
|
use Laravel\Nova\Menu\MenuItem;
|
||||||
|
use Laravel\Nova\Menu\MenuSection;
|
||||||
use Laravel\Nova\Nova;
|
use Laravel\Nova\Nova;
|
||||||
use Laravel\Nova\NovaApplicationServiceProvider;
|
use Laravel\Nova\NovaApplicationServiceProvider;
|
||||||
|
|
||||||
@@ -17,7 +27,27 @@ public function boot(): void
|
|||||||
{
|
{
|
||||||
parent::boot();
|
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(),
|
||||||
|
];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +70,7 @@ protected function fortify(): void
|
|||||||
protected function routes(): void
|
protected function routes(): void
|
||||||
{
|
{
|
||||||
Nova::routes()
|
Nova::routes()
|
||||||
->withAuthenticationRoutes(default: true)
|
->withAuthenticationRoutes(default: false)
|
||||||
->withPasswordResetRoutes()
|
->withPasswordResetRoutes()
|
||||||
->withEmailVerificationRoutes()
|
->withEmailVerificationRoutes()
|
||||||
->register();
|
->register();
|
||||||
@@ -54,9 +84,7 @@ protected function routes(): void
|
|||||||
protected function gate(): void
|
protected function gate(): void
|
||||||
{
|
{
|
||||||
Gate::define('viewNova', function (User $user) {
|
Gate::define('viewNova', function (User $user) {
|
||||||
return in_array($user->email, [
|
return $user->role?->name === 'admin';
|
||||||
'jonathan@blijnder.nl',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
|
->withSchedule(function (\Illuminate\Console\Scheduling\Schedule $schedule): void {
|
||||||
|
$schedule->job(\App\Jobs\CloseSessionsJob::class)->hourly();
|
||||||
|
})
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
@@ -22,5 +25,19 @@
|
|||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->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();
|
})->create();
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
"keywords": ["laravel", "framework"],
|
"keywords": ["laravel", "framework"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.4",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"inertiajs/inertia-laravel": "^2.0",
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/nova": "^5.0",
|
"laravel/nova": "^5.0",
|
||||||
"laravel/socialite": "^5.24",
|
"laravel/socialite": "^5.24",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10.1",
|
||||||
|
"maatwebsite/laravel-nova-excel": "^1.3",
|
||||||
|
"socialiteproviders/microsoft-azure": "^5.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
1269
composer.lock
generated
@@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
'name' => env('APP_NAME', 'Laravel'),
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
'version' => '1.0.0',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Environment
|
| Application Environment
|
||||||
|
|||||||
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,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
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,
|
||||||
|
|
||||||
|
];
|
||||||
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'
|
||||||
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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
@@ -9,7 +11,7 @@
|
|||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||||
*/
|
*/
|
||||||
class UserFactory extends Factory
|
final class UserFactory extends Factory
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The current password being used by the factory.
|
* The current password being used by the factory.
|
||||||
@@ -27,8 +29,10 @@ public function definition(): array
|
|||||||
'name' => fake()->name(),
|
'name' => fake()->name(),
|
||||||
'email' => fake()->unique()->safeEmail(),
|
'email' => fake()->unique()->safeEmail(),
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => self::$password ??= Hash::make('password'),
|
||||||
'remember_token' => Str::random(10),
|
'remember_token' => Str::random(10),
|
||||||
|
'job_title' => fake()->jobTitle(),
|
||||||
|
'company_name' => fake()->company(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -13,10 +15,17 @@ public function up(): void
|
|||||||
{
|
{
|
||||||
Schema::create('users', function (Blueprint $table) {
|
Schema::create('users', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
$table->foreignId('role_id')->default(1)->constrained();
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
|
$table->string('azure_id')->nullable()->unique();
|
||||||
|
$table->string('photo')->nullable();
|
||||||
|
$table->string('job_title')->nullable();
|
||||||
|
$table->string('department')->nullable();
|
||||||
|
$table->string('company_name')->nullable();
|
||||||
|
$table->string('phone')->nullable();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password')->nullable();
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public function up(): void
|
|||||||
$table->string('status', 50)->default('in_progress');
|
$table->string('status', 50)->default('in_progress');
|
||||||
$table->integer('score')->nullable();
|
$table->integer('score')->nullable();
|
||||||
$table->string('result', 50)->nullable();
|
$table->string('result', 50)->nullable();
|
||||||
$table->json('basic_info')->nullable();
|
|
||||||
$table->text('additional_comments')->nullable();
|
$table->text('additional_comments')->nullable();
|
||||||
$table->timestamp('completed_at')->nullable();
|
$table->timestamp('completed_at')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|||||||
413
database/seeders/AuditQuestionSeeder.php
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds question groups and questions for the Audit category.
|
||||||
|
* Assumes the Audit category already exists (created by CategorySeeder).
|
||||||
|
*/
|
||||||
|
final class AuditQuestionSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed all Audit question groups and their questions.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categoryId = DB::table('categories')->where('name', 'Audit')->value('id');
|
||||||
|
|
||||||
|
if ($categoryId === null) {
|
||||||
|
$categoryId = DB::table('categories')->insertGetId([
|
||||||
|
'name' => 'Audit',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->seedOpportunityDetails($categoryId);
|
||||||
|
$this->seedClientBackgroundAndHistory($categoryId);
|
||||||
|
$this->seedFinancialInformation($categoryId);
|
||||||
|
$this->seedRegulatoryCompliance($categoryId);
|
||||||
|
$this->seedRiskAssessment($categoryId);
|
||||||
|
$this->seedResourceAllocation($categoryId);
|
||||||
|
$this->seedReportngRequirements($categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 1: Opportunity Details (not scored, no answer options).
|
||||||
|
*/
|
||||||
|
private function seedOpportunityDetails(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Opportunity Details',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What sort of audit opportunity is it?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'How many locations involved in this opportunity?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'List any locations included in this opportunity where we do not have a Baker Tilly firm.',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Where is the client HQ?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Who is the competition?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 2: Client Background and History (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedClientBackgroundAndHistory(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Client Background and History',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What is the client\'s business and industry?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'There have been no significant changes in the client\'s business operations or structure recently?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Does the sector and/or client come with a reputation which we are comfortable that Baker Tilly is associated with?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => null,
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any previous audit reports or findings that need to be considered?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 3: Financial Information (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedFinancialInformation(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Financial Information',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client provided financial statements or balance sheet?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are the client\'s financial statements complete and accurate?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 4: Regulatory Compliance (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedRegulatoryCompliance(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Regulatory Compliance',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Does the client comply with all relevant regulatory requirements and standards?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'The client has no pending legal or regulatory issues that you know of that could impact the audit?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'The client has been subject to no regulatory investigations or penalties?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 5: Risk Assessment (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedRiskAssessment(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Risk Assessment',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'There are no key risks associated with the audit?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have you completed a conflict check?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are you and other BTI member firms independent withi the meaning of local and IESBA rules?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 6: Resource Allocation (scored, mixed — Q1 has no answer options and is not scored).
|
||||||
|
*/
|
||||||
|
private function seedResourceAllocation(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Resource Allocation',
|
||||||
|
'sort_order' => 6,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What resources are required for the audit (personnel, time, budget)?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Does your firm have the scale, seniority and degree of expertise available at the riht time to report in accordance with the client\'s schedule?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 7: Reportng Requirements (scored, Yes/No options — group name preserves Excel typo).
|
||||||
|
*/
|
||||||
|
private function seedReportngRequirements(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Reportng Requirements',
|
||||||
|
'sort_order' => 7,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do we understand reporting rules, regulatory environment and stakeholder expectations?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
database/seeders/CategorySeeder.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class CategorySeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the 6 fixed assessment categories.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categories = [
|
||||||
|
['name' => 'Audit', 'sort_order' => 1],
|
||||||
|
['name' => 'Outsource', 'sort_order' => 2],
|
||||||
|
['name' => 'Solution', 'sort_order' => 3],
|
||||||
|
['name' => 'Digital Solutions', 'sort_order' => 4],
|
||||||
|
['name' => 'Legal', 'sort_order' => 5],
|
||||||
|
['name' => 'Tax', 'sort_order' => 6],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($categories as $category) {
|
||||||
|
DB::table('categories')->insert([
|
||||||
|
'name' => $category['name'],
|
||||||
|
'sort_order' => $category['sort_order'],
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
final class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
use WithoutModelEvents;
|
use WithoutModelEvents;
|
||||||
|
|
||||||
@@ -14,6 +16,13 @@ class DatabaseSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->call(JonathanSeeder::class);
|
$this->call([
|
||||||
|
JonathanSeeder::class,
|
||||||
|
AuditQuestionSeeder::class,
|
||||||
|
DigitalSolutionsQuestionSeeder::class,
|
||||||
|
LegalQuestionSeeder::class,
|
||||||
|
OutsourceQuestionSeeder::class,
|
||||||
|
TaxQuestionSeeder::class,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
345
database/seeders/DigitalSolutionsQuestionSeeder.php
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds question groups and questions for the Digital Solutions category.
|
||||||
|
* Creates the category if it does not already exist.
|
||||||
|
*/
|
||||||
|
final class DigitalSolutionsQuestionSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed all Digital Solutions question groups and their questions.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categoryId = DB::table('categories')->where('name', 'Digital Solutions')->value('id');
|
||||||
|
|
||||||
|
if ($categoryId === null) {
|
||||||
|
$categoryId = DB::table('categories')->insertGetId([
|
||||||
|
'name' => 'Digital Solutions',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->seedOpportunityDetails($categoryId);
|
||||||
|
$this->seedClientBackgroundAndHistory($categoryId);
|
||||||
|
$this->seedRegulatoryCompliance($categoryId);
|
||||||
|
$this->seedRiskAssessment($categoryId);
|
||||||
|
$this->seedResourceAllocation($categoryId);
|
||||||
|
$this->seedTechnologyAndInnovationFit($categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 1: Opportunity Details (not scored, no answer options, all details required).
|
||||||
|
*/
|
||||||
|
private function seedOpportunityDetails(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Opportunity Details',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What sort of digital consulting opportunity is it?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'How many locations involved in this opportunity and are there any locations where we do not have digital capabilites in the local Baker Tilly firm.',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Where is the client HQ? please share more about the clients industry and digital maturity level.',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Who are the competitors in this space?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 2: Client Background and History (scored, Yes/No/NA options).
|
||||||
|
*/
|
||||||
|
private function seedClientBackgroundAndHistory(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Client Background and History',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have we previously worked with this client, and was the experience positive?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have we conducted a reputational risk check on the client (negative press, ethical concerns, etc.)?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 3: Regulatory Compliance (scored, Yes/No/NA options).
|
||||||
|
*/
|
||||||
|
private function seedRegulatoryCompliance(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Regulatory Compliance',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Does the project involve cross-border data transfers, and if so, are necessary safeguards in place?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Does the client have no pending legal, tax or regulatory issues that [you know of] which could impact this opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 4: Risk Assessment (scored, Yes/No/NA options).
|
||||||
|
*/
|
||||||
|
private function seedRiskAssessment(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Risk Assessment',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Is there a clear understanding of the project scope, responsibilities, and deliverables?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do we have the necessary delivery tools (platforms, technology, security measures etc.) to support this opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have we completed a conflict check?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Can we meet the service-level agreements (SLAs) without overcommitting our resources?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there no special expectations or requirements from the client that may pose a challenge?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 5: Resource Allocation (scored, Yes/No/NA options).
|
||||||
|
*/
|
||||||
|
private function seedResourceAllocation(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Resource Allocation',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do you have the resources required for the opportunity (personnel, time, budget)?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do you have the right expertise and capacity across our network to deliver high-quality service?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 6: Technology & Innovation Fit (scored, Yes/No/NA options, semicolons in scoring instructions).
|
||||||
|
*/
|
||||||
|
private function seedTechnologyAndInnovationFit(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Technology & Innovation Fit',
|
||||||
|
'sort_order' => 6,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point; if you answer no, you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are the technologies involved within our area of expertise, or do we have partnerships to support the implementation?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,32 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\Role;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class JonathanSeeder extends Seeder
|
final class JonathanSeeder extends Seeder
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Seed the application's database.
|
* Seed the application's database with admin user Jonathan.
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
User::factory()->create([
|
$adminRole = Role::where('name', 'admin')->first();
|
||||||
|
|
||||||
|
DB::table('users')->insert([
|
||||||
'name' => 'Jonathan',
|
'name' => 'Jonathan',
|
||||||
'email' => 'jonathan@blijnder.nl',
|
'email' => 'jonathan.van.rij@agerion.nl',
|
||||||
'password' => bcrypt('secret'),
|
'password' => bcrypt('secret'),
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
|
'role_id' => $adminRole->id,
|
||||||
|
'job_title' => 'Senior Developer',
|
||||||
|
'company_name' => 'Baker Tilly',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
506
database/seeders/LegalQuestionSeeder.php
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds question groups and questions for the Legal category.
|
||||||
|
* Assumes the Legal category already exists or creates it (sort_order=5).
|
||||||
|
*/
|
||||||
|
final class LegalQuestionSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed all Legal question groups and their questions.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categoryId = DB::table('categories')->where('name', 'Legal')->value('id');
|
||||||
|
|
||||||
|
if ($categoryId === null) {
|
||||||
|
$categoryId = DB::table('categories')->insertGetId([
|
||||||
|
'name' => 'Legal',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->seedOpportunityDetails($categoryId);
|
||||||
|
$this->seedClientBackgroundAndHistory($categoryId);
|
||||||
|
$this->seedFinancialInformation($categoryId);
|
||||||
|
$this->seedRegulatoryCompliance($categoryId);
|
||||||
|
$this->seedRiskAssessmentForLegalOpportunities($categoryId);
|
||||||
|
$this->seedResourceAllocation($categoryId);
|
||||||
|
$this->seedStakeholderEngagement($categoryId);
|
||||||
|
$this->seedFeeQuote($categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 1: Opportunity Details (mixed — some text-only, some Yes/No).
|
||||||
|
*/
|
||||||
|
private function seedOpportunityDetails(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Opportunity Details',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What type of legal opportunity is it (e.g., litigation, corporate, M&A, regulatory)?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'How many locations involved in this opportunity?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do we have a presence or a reliable partner in all locations listed in this opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Is the client budget realistic?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client requested any additional information from our firms?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What is the deadline to respond to the client on this opportunity?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 6,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Where is the client HQ?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 7,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Who is the competition?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 8,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 2: Client Background and History (scored, mixed Yes/No and text-only).
|
||||||
|
*/
|
||||||
|
private function seedClientBackgroundAndHistory(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Client Background and History',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What is the client\'s business and industry?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have there been any significant changes in the client\'s business operations or structure recently?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What is our competitive edge in this opportunity (e.g., prior experience with the client, unique expertise, pricing advantage)?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 3: Financial Information (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedFinancialInformation(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Financial Information',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client provided enough financial information about their company?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any significant financial risks or uncertainties that you are aware of?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 4: Regulatory Compliance (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedRegulatoryCompliance(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Regulatory Compliance',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any pending legal or regulatory issues that you know of that could impact the opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client been subject to any regulatory investigations or penalties?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 5: Risk Assessment for Legal opportunities (scored, Yes/No options).
|
||||||
|
* Group name preserves Excel casing exactly — lowercase 'o' in "opportunities".
|
||||||
|
*/
|
||||||
|
private function seedRiskAssessmentForLegalOpportunities(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Risk Assessment for Legal opportunities',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any potential risks or challenges associated with the opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has a conflict check been completed?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any potential conflicts of interest?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 6: Resource Allocation (mixed — Q2 and Q5 are text-only, others Yes/No).
|
||||||
|
*/
|
||||||
|
private function seedResourceAllocation(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Resource Allocation',
|
||||||
|
'sort_order' => 6,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do we have the required skills and capacity within our firm to deliver this work, or would we need support from another firm?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What resources are required for the opportunity (personnel, time, budget)?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any constraints on the availability of your resources?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do you know of the any constraints on the availability of other firms included in this opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Is the deadline to respond to the client is more than two weeks away. Our experience shows that anything shorter is often unrealistic to pursue.',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 7: Stakeholder Engagement (mixed — Q1 is text-only, Q2 is Yes/No).
|
||||||
|
*/
|
||||||
|
private function seedStakeholderEngagement(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Stakeholder Engagement',
|
||||||
|
'sort_order' => 7,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Who are the key stakeholders involved in this opportunity?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any special expectations and requirements?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 8: Fee Quote (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedFeeQuote(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Fee Quote',
|
||||||
|
'sort_order' => 8,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client provided sufficient information to enable a fee quote?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
349
database/seeders/OutsourceQuestionSeeder.php
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds question groups and questions for the Outsource category.
|
||||||
|
* Creates the Outsource category if it does not already exist.
|
||||||
|
*/
|
||||||
|
final class OutsourceQuestionSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed all Outsource question groups and their questions.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categoryId = DB::table('categories')->where('name', 'Outsource')->value('id');
|
||||||
|
|
||||||
|
if ($categoryId === null) {
|
||||||
|
$categoryId = DB::table('categories')->insertGetId([
|
||||||
|
'name' => 'Outsource',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->seedOpportunityDetails($categoryId);
|
||||||
|
$this->seedClientBackgroundAndHistory($categoryId);
|
||||||
|
$this->seedRegulatoryCompliance($categoryId);
|
||||||
|
$this->seedRiskAssessment($categoryId);
|
||||||
|
$this->seedResourceAllocation($categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 1: Opportunity Details (not scored, no answer options, details required).
|
||||||
|
*/
|
||||||
|
private function seedOpportunityDetails(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Opportunity Details',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What sort of outsourcing opportunity is it?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'How many locations involved in this opportunity?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'List any locations included in this opportunity where we do not have a Baker Tilly firm.',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Where is the client HQ?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What is the client\'s business and industry?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Who are the competitors in this space?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 6,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 2: Client Background and History (scored, Yes/No/NA options).
|
||||||
|
*/
|
||||||
|
private function seedClientBackgroundAndHistory(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Client Background and History',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have we previously worked with this client, and was the experience positive?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have we conducted a reputational risk check on the client (negative press, ethical concerns, etc.)?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 3: Regulatory Compliance (scored, Yes/No/NA options).
|
||||||
|
*/
|
||||||
|
private function seedRegulatoryCompliance(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Regulatory Compliance',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Does the client comply with all relevant regulatory requirements?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client provided complete and accurate financial records for review?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Does the client have no pending legal, tax or regulatory issues that [you know of] which could impact this opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 4: Risk Assessment (scored, Yes/No/NA options).
|
||||||
|
*/
|
||||||
|
private function seedRiskAssessment(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Risk Assessment',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Is there a clear understanding on the scope, responsibilities and deliverables?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do we have the necessary delivery tools (platforms, technology, security measures etc.) to support this opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have you completed a conflict check?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Can we meet the service-level agreements (SLAs) without overcommitting our resources?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there no special expectations or requirements from the client that may pose a challenge?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 5: Resource Allocation (scored, Yes/No/NA options).
|
||||||
|
*/
|
||||||
|
private function seedResourceAllocation(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Resource Allocation',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => 'If you answer yes, you will score 1 point, if you answer no you will score 0 points',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do you have the resources required for the opportunity (personnel, time, budget)?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do you have the right expertise and capacity across our network to deliver high-quality service?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
1761
database/seeders/QuestionSeeder.php
Normal file
449
database/seeders/TaxQuestionSeeder.php
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds question groups and questions for the Tax category.
|
||||||
|
* Assumes the Tax category is created if not already present.
|
||||||
|
*/
|
||||||
|
final class TaxQuestionSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed all Tax question groups and their questions.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categoryId = DB::table('categories')->where('name', 'Tax')->value('id');
|
||||||
|
|
||||||
|
if ($categoryId === null) {
|
||||||
|
$categoryId = DB::table('categories')->insertGetId([
|
||||||
|
'name' => 'Tax',
|
||||||
|
'sort_order' => 6,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->seedOpportunityDetails($categoryId);
|
||||||
|
$this->seedClientBackgroundAndHistory($categoryId);
|
||||||
|
$this->seedFinancialInformation($categoryId);
|
||||||
|
$this->seedRegulatoryCompliance($categoryId);
|
||||||
|
$this->seedRiskAssessment($categoryId);
|
||||||
|
$this->seedResourceAllocation($categoryId);
|
||||||
|
$this->seedStakeholderEngagement($categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 1: Opportunity Details (mixed — some scored with Yes/No, some text-only).
|
||||||
|
*/
|
||||||
|
private function seedOpportunityDetails(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Opportunity Details',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What sort of opportunity is it?/Describe the Scope of Work',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'How many locations involved in this opportunity?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do we have a Baker Tilly firm in all locations within this opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client requested any additional information from our firms?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What is the deadline to respond to the client on this opportunity?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Where is the client HQ?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 6,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Who is the competition?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 7,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 2: Client Background and History (mixed — Q1 text-only, Q2/Q3 scored Yes/No).
|
||||||
|
*/
|
||||||
|
private function seedClientBackgroundAndHistory(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Client Background and History',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What is the client\'s business and industry?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Have there been any significant changes in the client\'s business operations or structure recently?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Is the client an existing client?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 3: Financial Information (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedFinancialInformation(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Financial Information',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client provided enough financial information about their company?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any significant financial risks or uncertainties that you are aware of?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 4: Regulatory Compliance (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedRegulatoryCompliance(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Regulatory Compliance',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Does the client comply with all relevant regulatory requirements and standards?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_no',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any pending legal or regulatory issues that you know of that could impact the opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Has the client been subject to any regulatory investigations or penalties?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 5: Risk Assessment (scored, Yes/No options).
|
||||||
|
*/
|
||||||
|
private function seedRiskAssessment(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Risk Assessment',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any potential risks or challenges associated with the opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any potential conflicts of interest?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'req_on_yes',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 6: Resource Allocation (mixed — Q1/Q4 text-only, Q2/Q3 scored Yes/No).
|
||||||
|
*/
|
||||||
|
private function seedResourceAllocation(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Resource Allocation',
|
||||||
|
'sort_order' => 6,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What resources are required for the opportunity (personnel, time, budget)?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any constraints on the availability of your resources?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Do you know of the any constraints on the availability of other firms included in this opportunity?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'What is the expected timeline for the opportunity, including any critical deadlines that must be met?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed Group 7: Stakeholder Engagement (mixed — Q1 text-only, Q2 scored Yes/No).
|
||||||
|
*/
|
||||||
|
private function seedStakeholderEngagement(int $categoryId): void
|
||||||
|
{
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Stakeholder Engagement',
|
||||||
|
'sort_order' => 7,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('questions')->insert([
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Who are the key stakeholders involved in this opportunity?',
|
||||||
|
'has_yes' => false,
|
||||||
|
'has_no' => false,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'required',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_scored' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => 'Are there any special expectations and requirements?',
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => false,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
database/seeders/TestCategorySeeder.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class TestCategorySeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed a minimal test category with one question group and five scored questions for quick testing.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categoryId = DB::table('categories')->insertGetId([
|
||||||
|
'name' => 'Test',
|
||||||
|
'sort_order' => 99,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$groupId = DB::table('question_groups')->insertGetId([
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'name' => 'Test Group',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'description' => null,
|
||||||
|
'scoring_instructions' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$questions = [];
|
||||||
|
for ($i = 1; $i <= 5; $i++) {
|
||||||
|
$questions[] = [
|
||||||
|
'question_group_id' => $groupId,
|
||||||
|
'text' => "Test question {$i}",
|
||||||
|
'has_yes' => true,
|
||||||
|
'has_no' => true,
|
||||||
|
'has_na' => true,
|
||||||
|
'details' => 'optional',
|
||||||
|
'sort_order' => $i,
|
||||||
|
'is_scored' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('questions')->insert($questions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,7 +165,7 @@ ## Step 5: Page Stubs and Click-Through Flow
|
|||||||
|
|
||||||
## Step 6: Seeders -- Categories, Question Groups, and Questions
|
## Step 6: Seeders -- Categories, Question Groups, and Questions
|
||||||
|
|
||||||
[ ] **Seed all reference data: pre-screening questions, 6 categories, their question groups, and all questions with correct field configuration.**
|
[x] **Seed all reference data: pre-screening questions, 6 categories, their question groups, and all questions with correct field configuration.**
|
||||||
|
|
||||||
Create `DatabaseSeeder` (or dedicated seeders) for: the 10 pre-screening Yes/No questions (stored in config or a seeder — these are not in the `questions` table, they are handled by the screening flow), the 6 categories (Audit, Outsource, Solution, Digital Solutions, Legal, Tax) with correct `sort_order`, all question groups per category with names, descriptions, and `scoring_instructions` where applicable, and all questions with the correct `has_yes`, `has_no`, `has_na`, `details`, `is_scored`, and `sort_order` values. Source question data from `docs/questions-audit.md`, `docs/questions-outsource-solutions.md`, `docs/questions-digital-solutions.md`, `docs/questions-legal.md`, `docs/questions-tax.md`.
|
Create `DatabaseSeeder` (or dedicated seeders) for: the 10 pre-screening Yes/No questions (stored in config or a seeder — these are not in the `questions` table, they are handled by the screening flow), the 6 categories (Audit, Outsource, Solution, Digital Solutions, Legal, Tax) with correct `sort_order`, all question groups per category with names, descriptions, and `scoring_instructions` where applicable, and all questions with the correct `has_yes`, `has_no`, `has_na`, `details`, `is_scored`, and `sort_order` values. Source question data from `docs/questions-audit.md`, `docs/questions-outsource-solutions.md`, `docs/questions-digital-solutions.md`, `docs/questions-legal.md`, `docs/questions-tax.md`.
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ ## Step 6: Seeders -- Categories, Question Groups, and Questions
|
|||||||
|
|
||||||
## Step 7: Landing Page and Pre-Screening Flow
|
## Step 7: Landing Page and Pre-Screening Flow
|
||||||
|
|
||||||
[ ] **Build the real landing page and pre-screening questionnaire.**
|
[x] **Build the real landing page and pre-screening questionnaire.**
|
||||||
|
|
||||||
Build the Landing page: describes the application purpose (what Go/No Go is, what the user will do), with a "Continue" button that creates a screening and redirects to the pre-screening questions. Build the Screening/Show page: render the 10 Yes/No pre-screening questions using `useForm`. Save screening answers via `PUT /screening/{screening}`. On submit, calculate the screening score (Yes=1 point each), determine pass/fail (>=5 = pass), and redirect to the screening result page. Build Screening/Result: if failed (<5 points), show No Go result with "Again" button back to `/`. If passed, show the category picker (list of 6 categories) with "Start" buttons. Selecting a category creates a session linked to this screening and redirects to Session/Show.
|
Build the Landing page: describes the application purpose (what Go/No Go is, what the user will do), with a "Continue" button that creates a screening and redirects to the pre-screening questions. Build the Screening/Show page: render the 10 Yes/No pre-screening questions using `useForm`. Save screening answers via `PUT /screening/{screening}`. On submit, calculate the screening score (Yes=1 point each), determine pass/fail (>=5 = pass), and redirect to the screening result page. Build Screening/Result: if failed (<5 points), show No Go result with "Again" button back to `/`. If passed, show the category picker (list of 6 categories) with "Start" buttons. Selecting a category creates a session linked to this screening and redirects to Session/Show.
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ ## Step 7: Landing Page and Pre-Screening Flow
|
|||||||
|
|
||||||
## Step 8: Basic Info Form
|
## Step 8: Basic Info Form
|
||||||
|
|
||||||
[ ] **Build the basic info step as the first section of Session/Show.**
|
[x] **Build the basic info step as the first section of Session/Show.**
|
||||||
|
|
||||||
Add the basic info form fields to the Session/Show page: `client_name`, `client_contact`, `lead_firm_name`, `lead_firm_contact`. Use Inertia `useForm` to save data to the session's `basic_info` JSON column via `PUT /sessions/{session}`. All four fields are required before the user can proceed to questions. Display validation errors inline.
|
Add the basic info form fields to the Session/Show page: `client_name`, `client_contact`, `lead_firm_name`, `lead_firm_contact`. Use Inertia `useForm` to save data to the session's `basic_info` JSON column via `PUT /sessions/{session}`. All four fields are required before the user can proceed to questions. Display validation errors inline.
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ ## Step 8: Basic Info Form
|
|||||||
|
|
||||||
## Step 9: Questionnaire Flow -- Question Rendering and Answer Saving
|
## Step 9: Questionnaire Flow -- Question Rendering and Answer Saving
|
||||||
|
|
||||||
[ ] **Build the full questionnaire UI with all 6 question patterns and answer persistence.**
|
[x] **Build the full questionnaire UI with all 6 question patterns and answer persistence.**
|
||||||
|
|
||||||
Create the `QuestionCard` component that renders questions based on their field configuration (see the 6 patterns in `docs/technical-requirements.md` section 5). Render all questions on a single scrollable page within `Session/Show` (not paginated per group). In phase 2, questions will be visually grouped by their question group with group headers and scoring instructions. Save answers via `PUT /sessions/{session}` using Inertia `useForm` with partial reloads (only reload answers/score, not the full question set). Handle `details` textarea visibility: show when `details` is `required` or `optional`; show conditionally for `req_on_yes` / `req_on_no` based on the selected value. Include the Additional Comments textarea at the bottom of the page.
|
Create the `QuestionCard` component that renders questions based on their field configuration (see the 6 patterns in `docs/technical-requirements.md` section 5). Render all questions on a single scrollable page within `Session/Show` (not paginated per group). In phase 2, questions will be visually grouped by their question group with group headers and scoring instructions. Save answers via `PUT /sessions/{session}` using Inertia `useForm` with partial reloads (only reload answers/score, not the full question set). Handle `details` textarea visibility: show when `details` is `required` or `optional`; show conditionally for `req_on_yes` / `req_on_no` based on the selected value. Include the Additional Comments textarea at the bottom of the page.
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@ ## Step 9: Questionnaire Flow -- Question Rendering and Answer Saving
|
|||||||
|
|
||||||
## Step 10: Scoring and Result
|
## Step 10: Scoring and Result
|
||||||
|
|
||||||
[ ] **Implement server-side scoring and the result page.**
|
[x] **Implement server-side scoring and the result page.**
|
||||||
|
|
||||||
Calculate the score server-side from `is_scored` answers: Yes=1, No=0, NA=excluded. Return the running score and current result threshold via Inertia props during the questionnaire. Build the `ScoreIndicator` into the questionnaire flow with live color updates (green 10+, yellow 5-9, red 1-4). Build the real `Session/Result` page showing the final GO / NO GO / Consult Leadership result with color coding and an "Again" button that returns to `/`. On session submission, calculate final score and persist `score`, `result`, `status=completed`, and `completed_at` to the session.
|
Calculate the score server-side from `is_scored` answers: Yes=1, No=0, NA=excluded. Return the running score and current result threshold via Inertia props during the questionnaire. Build the `ScoreIndicator` into the questionnaire flow with live color updates (green 10+, yellow 5-9, red 1-4). Build the real `Session/Result` page showing the final GO / NO GO / Consult Leadership result with color coding and an "Again" button that returns to `/`. On session submission, calculate final score and persist `score`, `result`, `status=completed`, and `completed_at` to the session.
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ ## Step 10: Scoring and Result
|
|||||||
|
|
||||||
## Step 11: Activity Logging
|
## Step 11: Activity Logging
|
||||||
|
|
||||||
[ ] **Add append-only activity logging for analytics.**
|
[x] **Add append-only activity logging for analytics.**
|
||||||
|
|
||||||
Create a logging service (or helper) that writes to the `logs` table. Integrate log writes into all relevant actions: `login`, `logout`, `screening_started`, `screening_completed`, `session_started`, `session_completed`, `session_abandoned`, `answer_saved`, `step_viewed`. Each log includes `user_id`, `session_id`, `category_id`, `action`, and `metadata` JSON as defined in `docs/technical-requirements.md` section 5 (logs table). The Log model should have no `updated_at` and should prevent updates/deletes.
|
Create a logging service (or helper) that writes to the `logs` table. Integrate log writes into all relevant actions: `login`, `logout`, `screening_started`, `screening_completed`, `session_started`, `session_completed`, `session_abandoned`, `answer_saved`, `step_viewed`. Each log includes `user_id`, `session_id`, `category_id`, `action`, and `metadata` JSON as defined in `docs/technical-requirements.md` section 5 (logs table). The Log model should have no `updated_at` and should prevent updates/deletes.
|
||||||
|
|
||||||
@@ -403,7 +403,7 @@ ## Step 11: Activity Logging
|
|||||||
|
|
||||||
## Step 12: Nova Resources and Policies
|
## Step 12: Nova Resources and Policies
|
||||||
|
|
||||||
[ ] **Create all Nova resources, policies, and Excel export actions.**
|
[x] **Create all Nova resources, policies, and Excel export actions.**
|
||||||
|
|
||||||
Create Nova resources: `CategoryResource`, `QuestionGroupResource`, `QuestionResource`, `ScreeningResource`, `SessionResource`, `AnswerResource`, `LogResource`. Create corresponding policies enforcing the permission matrix from `docs/technical-requirements.md` section 9 (most are read-only; only Question.text is editable). Apply field behaviors: all fields filterable, sortable, and copyable where applicable. Set menu visibility (only Question, Screening, Session, Log appear in sidebar). Install `maatwebsite/laravel-nova-excel` and add `DownloadExcel` action to every resource.
|
Create Nova resources: `CategoryResource`, `QuestionGroupResource`, `QuestionResource`, `ScreeningResource`, `SessionResource`, `AnswerResource`, `LogResource`. Create corresponding policies enforcing the permission matrix from `docs/technical-requirements.md` section 9 (most are read-only; only Question.text is editable). Apply field behaviors: all fields filterable, sortable, and copyable where applicable. Set menu visibility (only Question, Screening, Session, Log appear in sidebar). Install `maatwebsite/laravel-nova-excel` and add `DownloadExcel` action to every resource.
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ ### Root Level
|
|||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
- `docs/theming-templating-vue.md` - Design tokens, Tailwind config, layout, shared Vue components, icon and scoring color standards
|
- `docs/theming-templating-vue.md` - Design tokens, Tailwind config, layout, shared Vue components, RadioButtonGroup pill buttons, icon and scoring color standards
|
||||||
|
|
||||||
### Agents
|
### Agents
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ ### Score Legend
|
|||||||
|
|
||||||
| Color | Points | Decision |
|
| Color | Points | Decision |
|
||||||
|-------|--------|----------|
|
|-------|--------|----------|
|
||||||
| 🟢 Green | 10+ Points | GO |
|
| Green | 10+ Points | GO |
|
||||||
| 🟡 Yellow | 5-9 Points | Speak to SL or SSL leadership |
|
| Yellow | 5-9 Points | Speak to SL or SSL leadership |
|
||||||
| 🔴 Red | 1-5 Points | NO GO |
|
| Red | 1-5 Points | NO GO |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -25,11 +25,13 @@ ### Basic Information
|
|||||||
|
|
||||||
## 1. Opportunity Details
|
## 1. Opportunity Details
|
||||||
|
|
||||||
|
> *Not scored*
|
||||||
|
|
||||||
| # | Question | Details |
|
| # | Question | Details |
|
||||||
|---|----------|---------|
|
|---|----------|---------|
|
||||||
| 8 | What sort of audit opportunity is it? | [insert details] |
|
| 8 | What sort of audit opportunity is it? | [insert details] |
|
||||||
| 9 | How many locations involved in this opportunity? | [insert details] |
|
| 9 | How many locations involved in this opportunity? | [insert details] |
|
||||||
| 10 | List any locations included in this opportunity where we do not have a Baker Tilly firm. | [if no insert details] |
|
| 10 | List any locations included in this opportunity where we do not have a Baker Tilly firm. | [optional] |
|
||||||
| 11 | Where is the client HQ? | [insert details] |
|
| 11 | Where is the client HQ? | [insert details] |
|
||||||
| 12 | Who is the competition? | [insert details] |
|
| 12 | Who is the competition? | [insert details] |
|
||||||
|
|
||||||
@@ -39,12 +41,12 @@ ## 1. Client Background and History
|
|||||||
|
|
||||||
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
||||||
|
|
||||||
| # | Question | Yes / No / Not applicable | Insert details |
|
| # | Question | Answer Options | Details |
|
||||||
|---|----------|---------------------------|----------------|
|
|---|----------|----------------|---------|
|
||||||
| 14 | What is the client's business and industry? | | [insert details] |
|
| 14 | What is the client's business and industry? | — | [insert details] |
|
||||||
| 15 | There have been no significant changes in the client's business operations or structure recently? | - | [if no insert details] |
|
| 15 | There have been no significant changes in the client's business operations or structure recently? | Yes / No | [if no insert details] |
|
||||||
| 16 | Does the sector and/or client come with a reputation which we are comfortable that Baker Tilly is associated with? | - | |
|
| 16 | Does the sector and/or client come with a reputation which we are comfortable that Baker Tilly is associated with? | Yes / No | — |
|
||||||
| 17 | Are there any previous audit reports or findings that need to be considered? | | [if yes insert details] |
|
| 17 | Are there any previous audit reports or findings that need to be considered? | Yes / No | [if yes insert details] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -52,10 +54,10 @@ ## 2. Financial Information
|
|||||||
|
|
||||||
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
||||||
|
|
||||||
| # | Question | Yes / No / Not applicable | Insert details |
|
| # | Question | Answer Options | Details |
|
||||||
|---|----------|---------------------------|----------------|
|
|---|----------|----------------|---------|
|
||||||
| 19 | Has the client provided financial statements or balance sheet? | - | [insert details if needed] |
|
| 19 | Has the client provided financial statements or balance sheet? | Yes / No | [insert details if needed] |
|
||||||
| 20 | Are the client's financial statements complete and accurate? | - | [insert details if needed] |
|
| 20 | Are the client's financial statements complete and accurate? | Yes / No | [if yes insert details] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -63,11 +65,11 @@ ## 3. Regulatory Compliance
|
|||||||
|
|
||||||
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
||||||
|
|
||||||
| # | Question | Yes / No / Not applicable | Insert details |
|
| # | Question | Answer Options | Details |
|
||||||
|---|----------|---------------------------|----------------|
|
|---|----------|----------------|---------|
|
||||||
| 22 | Does the client comply with all relevant regulatory requirements and standards? | - | [if no insert details] |
|
| 22 | Does the client comply with all relevant regulatory requirements and standards? | Yes / No | [if no insert details] |
|
||||||
| 23 | The client has no pending legal or regulatory issues that you know of that could impact the audit? | | [if no insert details] |
|
| 23 | The client has no pending legal or regulatory issues that you know of that could impact the audit? | Yes / No | [if no insert details] |
|
||||||
| 24 | The client has been subject to no regulatory investigations or penalties? | - | [if no insert details] |
|
| 24 | The client has been subject to no regulatory investigations or penalties? | Yes / No | [if no insert details] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -75,11 +77,11 @@ ## 4. Risk Assessment
|
|||||||
|
|
||||||
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
||||||
|
|
||||||
| # | Question | Yes / No / Not applicable | Insert details |
|
| # | Question | Answer Options | Details |
|
||||||
|---|----------|---------------------------|----------------|
|
|---|----------|----------------|---------|
|
||||||
| 26 | There are no key risks associated with the audit? | - | [if no insert details] |
|
| 26 | There are no key risks associated with the audit? | Yes / No | [if no insert details] |
|
||||||
| 27 | Have you completed a conflict check? | - | [insert details] |
|
| 27 | Have you completed a conflict check? | Yes / No | [insert details] |
|
||||||
| 28 | Are you and other BTI member firms independent with the meaning of local and IESBA rules? | - | [if no insert details] |
|
| 28 | Are you and other BTI member firms independent withi the meaning of local and IESBA rules? | Yes / No | [if no insert details] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -87,20 +89,20 @@ ## 5. Resource Allocation
|
|||||||
|
|
||||||
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
||||||
|
|
||||||
| # | Question | Yes / No / Not applicable | Insert details |
|
| # | Question | Answer Options | Details |
|
||||||
|---|----------|---------------------------|----------------|
|
|---|----------|----------------|---------|
|
||||||
| 30 | What resources are required for the audit (personnel, time, budget)? | - | [insert details if available] |
|
| 30 | What resources are required for the audit (personnel, time, budget)? | — | [insert details if available] |
|
||||||
| 31 | 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? | | [insert details if needed] |
|
| 31 | Does your firm have the scale, seniority and degree of expertise available at the riht time to report in accordance with the client's schedule? | Yes / No | [insert details if needed] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Reporting Requirements
|
## 6. Reportng Requirements
|
||||||
|
|
||||||
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
> *If you answer yes, you will score 1 point, if you answer no you will score 0 points*
|
||||||
|
|
||||||
| # | Question | Yes / No / Not applicable | Insert details |
|
| # | Question | Answer Options | Details |
|
||||||
|---|----------|---------------------------|----------------|
|
|---|----------|----------------|---------|
|
||||||
| 33 | Do we understand reporting rules, regulatory environment and stakeholder expectations? | - | [insert details if needed] |
|
| 33 | Do we understand reporting rules, regulatory environment and stakeholder expectations? | Yes / No | [insert details if needed] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ ### Color Palette
|
|||||||
| Token | Hex | RGB | Tailwind Class | Usage |
|
| Token | Hex | RGB | Tailwind Class | Usage |
|
||||||
|-------------|-----------|------------------|-----------------|---------------------------------------------|
|
|-------------|-----------|------------------|-----------------|---------------------------------------------|
|
||||||
| Primary | `#d1ec51` | 209, 236, 81 | `bg-primary`, `text-primary` | Buttons (default), accents, highlights |
|
| Primary | `#d1ec51` | 209, 236, 81 | `bg-primary`, `text-primary` | Buttons (default), accents, highlights |
|
||||||
|
| Primary Dark | `#b5d136` | 181, 209, 54 | `bg-primary-dark`, `text-primary-dark` | Selected/hover state for pill buttons, ~15% darker primary |
|
||||||
| Secondary | `#00b7b3` | 0, 183, 179 | `bg-secondary`, `text-secondary` | Button hover states, secondary accents |
|
| Secondary | `#00b7b3` | 0, 183, 179 | `bg-secondary`, `text-secondary` | Button hover states, secondary accents |
|
||||||
| Background | `#2b303a` | 43, 48, 58 | `bg-surface` | Page background, card backgrounds |
|
| Background | `#2b303a` | 43, 48, 58 | `bg-surface` | Page background, card backgrounds |
|
||||||
| Text | `#ffffff` | 255, 255, 255 | `text-white` | Primary body text on dark background |
|
| Text | `#ffffff` | 255, 255, 255 | `text-white` | Primary body text on dark background |
|
||||||
@@ -96,6 +97,23 @@ ### QuestionCard
|
|||||||
- `has_na` -- show N/A button
|
- `has_na` -- show N/A button
|
||||||
- `details` -- show a text input for additional notes
|
- `details` -- show a text input for additional notes
|
||||||
|
|
||||||
|
### RadioButtonGroup
|
||||||
|
|
||||||
|
Pill-shaped button group that replaces native radio buttons. Options appear as connected segments with rounded outer edges.
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
|--------------|-------------------------------|---------|--------------------------------|
|
||||||
|
| `modelValue` | `String \| null` | `null` | Selected value (v-model) |
|
||||||
|
| `options` | `Array<{value, label}>` | required | Options to render |
|
||||||
|
| `name` | `String` | required | HTML radio group name |
|
||||||
|
| `disabled` | `Boolean` | `false` | Disables all options |
|
||||||
|
|
||||||
|
Default state: `bg-primary` with `text-gray-900`.
|
||||||
|
Selected & hover state: `bg-primary-dark`.
|
||||||
|
Keyboard focus: visible ring using `ring-primary-dark`.
|
||||||
|
|
||||||
|
Used in `QuestionCard` (3-option: Yes/No/N/A) and `Screening/Show` (2-option: Yes/No).
|
||||||
|
|
||||||
## Icons
|
## Icons
|
||||||
|
|
||||||
Heroicons is the only icon library. No other icon packages.
|
Heroicons is the only icon library. No other icon packages.
|
||||||
|
|||||||
BIN
no-pill-centered.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
1603
package-lock.json
generated
@@ -4,12 +4,16 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"dev": "vite"
|
"dev": "vite",
|
||||||
|
"cy:open": "cypress open",
|
||||||
|
"cy:run": "cypress run",
|
||||||
|
"test:e2e": "cypress run"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
|
"cypress": "^15.9.0",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"vite": "^7.0.7"
|
"vite": "^7.0.7"
|
||||||
|
|||||||
BIN
pdf-preview-after.png
Normal file
|
After Width: | Height: | Size: 512 KiB |
BIN
pdf-preview-before.png
Normal file
|
After Width: | Height: | Size: 511 KiB |
1
public/images/baker-tilly-logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 598.24 157.147"><path fill="#fff" d="m540.007 130.878.429-11.135c-.612.136-2.959.512-4.54.512-5.277 0-6.71-3.427-6.71-8.124V47.525l-14.238 6.437v60.506c0 8.59 4.265 17.17 16.596 17.17 4.89 0 7.835-.64 8.463-.76zm-32.05 0 .431-11.135c-.613.136-2.961.512-4.541.512-5.278 0-6.708-3.427-6.708-8.124V47.525l-14.24 6.437v60.506c0 8.59 4.266 17.17 16.596 17.17 4.89 0 7.836-.64 8.462-.76zm-55.201-.182V72.293h14.24v58.403Zm7.01-65.621c-4.577 0-8.037-1.786-8.037-8.259 0-5.916 3.126-8.373 8.036-8.373 5.023 0 8.262 1.898 8.262 8.261 0 6.139-3.015 8.37-8.262 8.37zm85.802 92.072c.515 0 3.365-.479 4.115-.595 14.004-2.146 24.264-8.333 29.914-25.426 3.847-11.636 18.65-58.798 18.65-58.798H583.14l-13.602 45.95h-2.42l-11.6-45.95h-14.771l15.804 58.398h9.095s-.356 1.162-.624 2.08c-1.898 6.502-10.256 11.635-21.355 13.222zm-160.67-26.455v-46.13c1.743-.58 5.835-1.278 10.715-1.278 2.558 0 7.32.192 9.414.424V71.435c-1.628-.464-6.03-.602-10.098-.602-10.228 0-19.22 2.585-24.27 4.838v55.021zm-37.193-35.301v-1.663c0-7.702-4.205-11.507-12.726-11.507-9.178 0-13.098 5.15-13.331 13.17zm-7.781 25.233c8.76 0 14.27-2.173 16.973-3.003l1.368 10.425c-2.272 1.047-9.72 4.154-19.37 4.154-21.154 0-31.502-10.843-31.502-31.762 0-21.16 9.868-29.722 27.982-29.722 19.749 0 25.825 10.926 25.825 29.406 0 1.975-.136 4.439-.252 5.22h-39.53c0 10.546 6.615 15.282 18.506 15.282zm-71.09 10.068v-26.69h4.45l19.527 26.69H308.7l-24.19-33.935 22.014-24.43h-15.632l-18.11 20.493h-3.949V47.27l-14.24 7.021v76.401zm161.566.949c6.196 0 9.647-.96 10.315-1.132l.656-11.275c-.539.21-4 1.057-6.03 1.057-7.156 0-9.384-5.085-9.384-11.826V84.632h17.224V72.328h-17.224V60.16l-14.237 6.478v45.51c0 13.384 6.988 19.492 18.68 19.492zm-201.83-25.923c-2.797-.445-8.417-1.17-12.705-1.17-10.1 0-11.762 4.683-11.762 8.329 0 5.358 3.496 7.792 12.166 7.792 5.662 0 10.208-.725 12.302-1.308zm-11.92 26.426c-18.713 0-25.755-6.154-25.755-18.89 0-12.995 6.856-18.8 23.926-18.8 5.962 0 12.404 1.009 13.75 1.196v-4.355c0-4.676-2.312-8.775-14.996-8.775-9.628 0-15.943 2.68-17.891 3.441l-1.1-10.856c2.568-1.012 10.775-4.389 23.048-4.389 18.453 0 25.168 7.87 25.168 23.022v34.197c-3.368 1.381-13.792 4.209-26.15 4.209zm-64.002-11.486c12.516 0 17.323-7.969 17.323-19.163 0-10.854-4.62-19.004-17.323-19.004-3.487 0-8.423 1.157-10.61 1.851v34.466c2.187.724 6.79 1.85 10.61 1.85zm-24.891-66.367 14.282-6.453v25.64c2.645-.93 7.526-2.762 15.661-2.762 17.643 0 26.21 9.396 26.21 30.78 0 25.057-9.837 30.704-32.44 30.704-10.809 0-19.595-1.986-23.713-3.7V54.29M70.175 2.867C66.105.857 59.805-.005 53.728-.005 26.208-.005 0 15.509 0 41.638c0 9.23 2.93 19.098 8.046 27.938 5.783 9.996 13.802 19.135 23.375 25.63 7.264 4.93 15.678 8.369 24.37 8.369 7.24 0 12.101-1.985 13.785-3.373a29.48 29.48 0 0 1-4.642.374c-23.7 0-39.964-20.123-45.153-35.15-1.914-5.543-3.917-12.547-3.917-19.345 0-27.818 24.451-43.092 50.117-43.092 1.362 0 2.733.044 4.11.14.106.007.174-.058.174-.132 0-.046-.029-.096-.09-.13zM74.4 44.778c-12.017 0-21.314 9.18-21.314 20.034 0 11.05 8.553 21.348 21.757 21.348 2.83 0 6.057-.837 8.88-1.944.489-.19 1.104-.421 1.515-.751.336-.272.647-.925.864-1.314 3.324-5.98 7.116-18.8 7.686-26.373.058-.776-.159-1.057-.62-1.684-3.12-4.223-9.192-9.316-18.768-9.316zm19.854.357c0 1.022-.022 3.224-.047 4.229-4.477-7.88-13.181-12.93-22.187-12.93-14.098 0-25.948 11.805-25.948 27.038 0 14.977 11.6 26.371 26.204 26.371 3.701 0 7.72-.849 10.63-2.323-1.457 2.135-2.47 3.453-3.96 5.112-.795.884-1.904 2.09-3.03 2.505-1.285.472-3.977 1.547-8.776 1.547-15.31 0-26.698-11.367-31.823-19.025-5.11-7.637-9.524-17.874-9.524-28.825 0-20.996 18.443-32.429 37.425-32.429 21.743 0 31.036 16.588 31.036 28.73"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/images/growth-symbol.png
Normal file
|
After Width: | Height: | Size: 82 KiB |