# hr360 — System Specification (Handover)

## 0. Document control

### 0.1 Purpose
This document specifies the complete functionality, user experience, data model, and runtime behavior of the **hr360** system (repository: `Meatco_hr`). It is intended as a handover-grade specification enabling another software developer/team to **rebuild the same system** (feature parity, UI parity, and data/business rules parity).

### 0.2 Scope
In scope:
- Public job portal (frontend)
- Admin panel (backoffice)
- Authentication & authorization (roles/permissions)
- Database schema and business rules
- File storage behavior (local/S3)
- Email/notifications behavior
- Zoom meeting integration for interviews
- Multilingual support (UI language + translation manager)
- Deployment constraints (notably shared hosting)

Out of scope:
- Infrastructure-specific implementation details not present in the repository (e.g., hosting provider control panel configuration)

### 0.3 Technology baseline (authoritative)
- **Language/runtime**: PHP 8+
- **Database**: MariaDB/MySQL 
- **Frontend (admin)**: AdminLTE + jQuery + DataTables


### 0.4 Repository top-level structure (high level)


---

## 1. System overview

### 1.1 What the system does
hr360 is a recruitment/job portal that allows:
- HR/Admin users to configure jobs, application workflows, questions, and settings.
- Candidates (public users) to browse job openings, view details, apply for roles, and respond to offer letters.
- HR/Admin users to manage applications in a pipeline (board/kanban + table view), schedule interviews (offline or online via Zoom), onboard successful candidates, and manage internal notes.

### 1.2 Primary actors
- **Candidate (Public)**: anonymous website user applying for a job.
- **Admin User**: authenticated user in admin panel.
- **Role-based user**: admin user with a specific role and permission set (Entrust).

### 1.3 Key domain objects
- Job
- Job Location
- Job Category
- Skill
- Job Application
- Application Status (pipeline column)
- Interview Schedule
- Onboarding / Offer (Onboard)
- User, Role, Permission

---

## 2. Entry points and request lifecycle


---

## 3. URL routes and modules (authoritative list)

### 3.1 Frontend (public job portal)
Route group:
- Namespace: `Front`
- Route name prefix: `jobs.`

Endpoints (from `routes/web.php`):
- `GET /` → `FrontJobsController@jobOpenings` (middleware: `disable-frontend`)
- `POST /more-data` → `FrontJobsController@moreData`
- `POST /search-job` → `FrontJobsController@searchJob`
- `GET /job-offer/{slug?}` → `FrontJobOfferController@index`
- `POST /save-offer` → `FrontJobOfferController@saveOffer`
- `GET /job/{slug}/{location?}/{hash?}` → `FrontJobsController@jobDetail`
- `GET /jobapply/{slug}/{location?}/{hash?}` → `FrontJobsController@jobApply`
- `POST /job/saveApplication` → `FrontJobsController@saveApplication`
- `POST /job/fetch-country-state` → `FrontJobsController@fetchCountryState`
- `POST /change-language/{code}` → `FrontJobsController@changeLanguage`
- `GET /auth/callback/{provider}` → `FrontJobsController@callback`
- `GET /auth/redirect/{provider}` → `FrontJobsController@redirect`
- `GET /job-alert` → `FrontJobsController@jobAlert`
- `POST /save-job-alert` → `FrontJobsController@saveJobAlert`
- `POST /disable-job-alert/{id}` → `FrontJobsController@disableJobAlert`
- `GET /{slug}` → `FrontJobsController@customPage` (custom footer pages)

Primary frontend Blade templates (from `resources/views/front/`):
- `front/job-openings.blade.php`
- `front/job-detail.blade.php`
- `front/job-apply.blade.php`
- `front/more_data.blade.php`
- `front/job-alert.blade.php`
- `front/custom-page.blade.php`
- `front/job-offer.blade.php`

### 3.2 Authentication
- `Auth::routes()` (Laravel UI auth scaffolding)
- Uses `resources/views/layouts/auth.blade.php` for the login-related pages.

### 3.3 Admin panel (authenticated)
Route group:
- Middleware: `auth`
- URL prefix: `/admin`
- Namespace: `Admin`
- Route name prefix: `admin.`

Major functional areas (route resources and notable custom endpoints):
- Dashboard: `GET /admin/dashboard`
- Job Categories: `job-categories` (with `data`, `getSkills/{categoryId}`)
- Questions: `questions` (with `data`)
- Todo: `todo-items` (with `updateTodoItem`)
- Job Alerts (admin viewing): `job_alert`
- Activity Logs: `logs` (with `data`)
- Settings group (`/admin/settings/...`):
  - Company settings: `settings`
  - Application setting: `application-setting`
  - Role/permission: `role-permission` (+ assign/remove/attach/detach)
  - Language settings: `language-settings` (+ change language/status)
  - Theme settings: `theme-settings` (+ disable frontend)
  - SMTP: `smtp-settings` (+ send test email)
  - SMS: `sms-settings`
  - LinkedIn: `linkedin-settings`
  - Footer pages: `footer-settings` (+ data)
  - Update application: `update-application`
- Zoom:
  - `zoom-meeting` (+ table view, start/end/cancel)
  - `zoom-setting`
  - `category`
- Master data:
  - Skills: `skills` (+ data)
  - Locations: `locations` (+ data)
  - Departments: `departments`
  - Designations: `designations`
  - Work experience: `work-experience`
  - Job type: `job-type`
  - Company: `company` (+ data)
  - Currency: `currency-settings`
- Jobs:
  - `jobs` (+ data, refresh-date, send-emails)
- Applications:
  - `job-applications` (board & table view, export, scheduling, archive/unarchive, drag/drop ordering)
  - `applications-archive` (data + export)
  - Application status/pipeline: `application-status`
- Interview schedules:
  - `interview-schedule` (data + table-view + status changes + notify/response)
- Onboarding:
  - `job-onboard` (data + send offer + update status)
  - `job-onboard-questions` (question bank)
- Documents:
  - `documents` (+ data + download)
- Reports: `report`
- Security settings: `/security-setting` (+ verifyCaptcha)
- Storage settings: `storage-settings`
- Sticky notes: `sticky-note`
- Applicant notes: `applicant-note`

---

## 4. Presentation layer specification (UI/UX)

### 4.1 Shared layout (Admin)
Template: `resources/views/layouts/app.blade.php`

Key UI elements:
- Top navbar:
  - Hamburger menu toggles sidebar
  - Profile link (route depends on `is_superadmin`)
  - Notifications dropdown (unread notifications)
  - Logout
- Left sidebar: included via `sections.left-sidebar`
- Breadcrumb: included via `sections.breadcrumb`
- Sticky notes footer bar:
  - Expand/collapse sticky notes list
  - Create/edit/delete note via AJAX modal `#responsive-modal`
- AJAX modals:
  - `#application-lg-modal` and `#application-md-modal`

Admin JS dependencies loaded:
- jQuery, Bootstrap, DataTables
- SweetAlert, Toastr
- Select2, Bootstrap-select
- Magnific Popup
- Froiden helper (`helper.js`), includes `$.easyAjax()` and `$.ajaxModal()` patterns

Theme customizations:
- Primary color via CSS variable `--main-color` from `$adminTheme->primary_color`
- Admin custom CSS injected from DB: `{!! $adminTheme->admin_custom_css !!}`

### 4.2 Shared layout (Frontend)
Template: `resources/views/layouts/front.blade.php`

Key UI elements:
- Topbar:
  - Company logo
  - Optional language switcher (`$global->front_language`)
  - Optional job alert button (`$global->job_alert_status`)
- Header hero background image from `$frontTheme->background_image_url`
- Footer:
  - Custom footer pages from `$customPages`, link to `jobs.custom-page`

Theme customizations:
- Primary color via `--main-color` from `$frontTheme->primary_color`
- Front custom CSS injected from DB: `{!! $frontTheme->front_custom_css !!}`

### 4.3 Auth pages layout
Template: `resources/views/layouts/auth.blade.php`

Key UI elements:
- Login box with company logo
- Background image: `assets/images/background/auth.jpg`
- Link button to “Visit Job Opening”

---

## 5. Business workflows (functional specification)

### 5.1 Public: Browse job openings
Controller: `FrontJobsController@jobOpenings`

Behavior:
- Only shows jobs where:
  - `jobs.status = 'active'`
  - `jobs.start_date <= today`
  - `jobs.end_date >= today` OR `jobs.end_date IS NULL`
- Uses `job_job_locations` join model (`JobJobLocation`) as the unit for listing so each job-location can be displayed.
- Front listing supports filtering by:
  - Location, category, company, skill
- Pagination behavior:
  - Initial load shows `perPage = 6`
  - "Load more" is implemented by AJAX (`/more-data`) and returns `front/more_data.blade.php`

### 5.2 Public: View job details
Route: `GET /job/{slug}/{location?}/{hash?}`

Expected behavior:
- Displays job description, requirements, and optionally salary/job type/work experience depending on job flags.
- Uses `jobs.meta_details` for OG/SEO tags where present.

Authoritative controller behavior (method: `FrontJobsController@jobDetail`):
- Job must satisfy:
  - `status = active`
  - `start_date <= now`
  - `end_date >= now`
- The `location` parameter is treated as a **location id** and is resolved via `job_job_locations`.
- Stores session values:
  - `lastPageUrl = slug`
  - `slug = slug`
- Sets OG/SEO:
  - `metaTitle`, `metaDescription` from `jobs.meta_details` (if present)
  - `metaImage` from the job company’s `logo_url`
  - `pageUrl` from `request()->url()`

Authoritative UI behavior (template: `front/job-detail.blade.php`):
- Main content:
  - Job title
  - Skills badges
  - Job description (HTML)
  - Job requirements (HTML)
- Sidebar cards:
  - Posted by (company name shown only if `companies.show_in_frontend = true`)
  - Locations (single resolved location display)
  - Job category
  - Skills list (again, in sidebar)
  - Total positions
  - Work experience (only if `jobs.show_work_experience = true`)
  - Job type (only if `jobs.show_job_type = true`)
  - Salary section (only if `jobs.show_salary = true`)
    - Rendering depends on `jobs.pay_type`: `Range`, `Starting`, `Maximum`, `Exact Amount`.
- QR code:
  - Renders a QR code which links to the apply route `jobs.jobApply(job.slug, location_id)`.
- Apply button:
  - Links to `jobs.jobApply(job.slug, location_id)`.
- Social sharing block:
  - LinkedIn, Facebook, Twitter, Google Plus, WhatsApp share links.
- LinkedIn sign-in CTA:
  - Shown only when `linked_in_settings.status = enable`.

### 5.3 Public: Apply for a job
Route: `GET /jobapply/{slug}/{location?}/{hash?}` (form) and `POST /job/saveApplication` (submit)

Business rules (DB-backed):
- Job controls which candidate fields are required via `jobs.required_columns` JSON:
  - `gender`, `dob`, `country`, `address`
- Job controls which sections are shown/required via `jobs.section_visibility` JSON:
  - `profile_image`, `resume`, `cover_letter`, `terms_and_conditions`
- Custom questions:
  - Questions attached to job via `job_questions`
  - Answers captured in `job_application_answers`

Notifications:
- New application triggers admin notifications (`App\Notifications\NewJobApplication`) to the admin set.

Authoritative UI behavior (template: `front/job-openings.blade.php`):
- Search/filter controls in the hero header:
  - Location (`#location_id`)
  - Category (`#category`)
  - Company (`#company`)
  - Skill (`#skill`)
- `SEARCH` button triggers AJAX to `POST jobs.search-job`.
- `Load more` button triggers AJAX to `POST jobs.more-data`.

Authoritative controller behavior (method: `FrontJobsController@jobApply`):
- Loads job by `slug` and checks `status=active`.
- Resolves the location parameter against `job_locations.id`.
- LinkedIn prefill:
  - If session contains `accessToken`, uses Socialite (`linkedin`) to fetch the user.
  - Candidate name/email/avatar can be prefilled into the form.
- Loads:
  - `$jobQuestion` (job->questions)
  - `$applicationSetting` (for legal terms)

Authoritative form behavior (template: `front/job-apply.blade.php`):
- Form contains hidden inputs:
  - `job_id`
  - `location_id`
- Conditional sections based on job configuration:
  - Candidate fields based on `jobs.required_columns`:
    - `address`, `gender`, `dob`, `country` (country includes state/city/zip)
  - Upload sections based on `jobs.section_visibility`:
    - Profile image (`photo`) and LinkedIn avatar behavior
    - Resume upload (`resume`)
    - Cover letter textarea
    - Terms & conditions (checkbox `term_agreement`)
- Additional job questions:
  - Each question is rendered as `answer[question_id]`.
  - `type=text` renders input.
  - `type=file` renders file input, but keeps a hidden input for `answer[question_id]`.
- reCAPTCHA:
  - If `google_captcha_settings.job_apply_page=active` AND master `status=active`, the captcha container is displayed.
  - Hidden input `recaptcha` is present for token submission.

Authoritative submission behavior (method: `FrontJobsController@saveApplication`):
- Creates a new `job_applications` record with:
  - `status_id = 1` (applied)
  - `column_priority = 0`
  - `location_id`:
    - uses request `location_id` if present
    - otherwise uses the first `job_job_locations` row for that job
- Country/state mapping:
  - Reads JSON files:
    - `public/country-state-city/countries.json`
    - `public/country-state-city/states.json`
  - Stores *names* in `job_applications.country` and `job_applications.state` (not numeric ids)
- Candidate photo:
  - If `photo` file is uploaded, stored via `Files::uploadLocalOrS3()` in `candidate-photos`.
  - If `linkedinPhoto` URL is provided, downloads the image and stores it under `candidate-photos/` and sets `job_applications.photo` to that filename.
- Resume:
  - If `resume` file uploaded, stored under `documents/{application_id}` and a polymorphic `documents` row is created with name `Resume`.
- Job question answers:
  - Creates `job_application_answers` rows, storing either text or uploaded files via `Files::uploadLocalOrS3()`.
- Notifications:
  - Notifies `User::allAdmins()` using `App\Notifications\NewJobApplication($jobApplication, $linkedin)`.
  - Sends an email `App\Mail\ReceivedApplication($jobApplication, $global)`.
- Response:
  - Returns JSON success payload via `Reply::dataOnly()`.

Job alert workflow (methods: `FrontJobsController@jobAlert`, `saveJobAlert`, `disableJobAlert`):
- Job alert modal is opened from the frontend layout button and loads `front/job-alert.blade.php`.
- Creating a job alert:
  - Persists in `job_alerts` with:
    - `email`, `work_experience_id`, `job_type_id`
    - `status=active`
    - `hash` = random 16 chars
  - Links categories via pivot `job_alert_categories`.
  - Links locations via pivot `job_alert_locations`.
- Disabling a job alert:
  - Sets `job_alerts.status = inactive` for the given id and redirects to job openings.

### 5.4 Admin: Manage jobs
Controller: `AdminJobsController`

Create job (`store`):
- Builds and persists:
  - `required_columns` boolean map
  - `section_visibility` yes/no map
  - `meta_details` title/description (defaults derived from job title/description)
- Links:
  - Skills via `job_skills`
  - Questions via `job_questions` (`questions()->sync()`)
  - Locations via `job_job_locations` (one row per selected location)
- Triggers `JobAlertEvent($job)` after save.

Update job (`update`):
- Recomputes and persists:
  - `required_columns`
  - `section_visibility`
  - `meta_details`
- Toggles:
  - `show_job_type`, `show_work_experience`, `show_salary` based on `yes` flags
- Resets and rebuilds relations:
  - Deletes and recreates `job_skills` for the job
  - Syncs `job_questions`
  - Deletes and recreates `job_job_locations`

Jobs list UI (DataTables, method: `AdminJobsController@data`):
- Filtering:
  - Company (`filter_company`)
  - Status (`filter_status`)
  - Location (`filter_location` against `job_job_locations.location_id`)
- Actions include:
  - Edit
  - Copy job URL (frontend job detail route)
  - Delete
  - Refresh expired job (if status `expired`)
  - Duplicate job (opens create screen with `?duplicate_job={id}`)

Bulk email job opening feature:
- Admin page: `AdminJobsController@sendEmail` → `admin.jobs.send-email`.
- Candidate listing for mailing uses `AdminJobsController@applicationData` (DataTables).
- Send behavior (`AdminJobsController@sendEmails`):
  - Requires a job selected (`job_for_email`).
  - Target set is either:
    - Selected application ids (`selectedIds`), or
    - All filtered applications (`allSelected=true`).
  - Optional `excludeSent=true`:
    - Excludes applicants already linked to the job via pivot `job_job_application` (Eloquent relationship `job->applications()`).
  - Links applications to job using `syncWithoutDetaching(application_ids)`.
  - Sends notification `App\Notifications\NewJobOpening($job)` to **unique emails** (deduplicated by applicant email).

### 5.5 Admin: Job applications pipeline (board)
Controller: `AdminJobApplicationController@index`

Key behaviors:
- Permission gate: `view_job_applications`
- Board columns come from `application_status` ordered by `position`.
- Each column counts applications and loads applications with filters:
  - Date range (`created_at` date)
  - Job
  - Search (`full_name`, `email`, `phone`)
  - Location (`job_applications.location_id`)
  - Company (join `jobs`)
  - Question filter (join `job_questions` and optionally `job_application_answers`)
  - Skill filter (uses `whereJsonContains('skills', ...)` on `job_applications.skills`)
- Board structure JSON is produced to support drag/drop ordering client-side.

Application status-change email rules (mail settings):
- Application-status notifications are gated by `application_settings.mail_setting`.
- Two places trigger `CandidateStatusChange`:
  - Drag/drop (`updateIndex`) when a task is moved between statuses and that status has mail enabled.
  - Edit form (`update`) when status changes (`status_id` dirty) and the target status has mail enabled.

Drag/drop ordering (method: `AdminJobApplicationController@updateIndex`):
- Inputs:
  - `applicationIds[]` — ordered list of application ids across columns
  - `boardColumnIds[]` — corresponding target `application_status.id`
  - `prioritys[]` — corresponding `column_priority` values
  - `draggedTaskId` — the single application id that was dragged (used for notification)
- Behavior:
  - Updates each application’s:
    - `column_priority`
    - `status_id`
  - Sends `CandidateStatusChange` to the candidate application model if mail is enabled for the target status and `draggedTaskId != 0`.

Board “load more” per column (method: `AdminJobApplicationController@loadMore`):
- Loads additional applications for a given status column (`columnId`) ordered by `column_priority`.
- Applies the same filter dimensions used by board view (dates, job, company, search, location, question/value, skills).
- Returns HTML fragment `admin.job-applications.load_more` plus a flag indicating whether to hide or show the load-more button.

Authoritative board UI behavior (template: `admin/job-applications/board.blade.php`):
- Actions row:
  - Toggle filters panel
  - Switch to Table View
  - Open Mail Settings modal (`admin.application-setting.modal`)
  - Create new application status (pipeline column)
- Search input:
  - Text search input with clear button
- Filters panel includes:
  - Date range (start/end)
  - Company
  - Job
  - Location
  - Skills (multi-select)
  - Question + optional question value input
  - Apply and Reset buttons
- “Load more” per column:
  - Column cards support incremental loading per status column.

Interview scheduling from applications (controller: `AdminJobApplicationController`):
- `createSchedule(id)` opens a modal view for scheduling (`admin.job-applications.interview-create`).
- `storeSchedule()`:
  - Parses `scheduleDate + scheduleTime` into a timestamp.
  - If interview type is `online`:
    - Creates a local `zoom_meetings` row and creates the actual Zoom meeting through Zoom API.
    - Updates `zoom_meetings.meeting_id`, `start_link`, `join_link`, `password`.
  - Creates an `interview_schedules` row.
  - Updates the job application status to the `application_status` row where `status='interview'`.
  - Saves optional schedule comment.
  - Attaches employee users as interviewers.
  - Sends notifications:
    - Interviewers: `ScheduleInterview($jobApplication, $meeting)`
    - Candidate: `CandidateScheduleInterview($jobApplication, $interviewSchedule, $meeting)`

### 5.6 Admin: Interview scheduling
Controller: `InterviewScheduleController`

Key behaviors:
- Schedules are filtered to exclude soft-deleted applications: `whereNull(job_applications.deleted_at)`
- Upcoming schedules grouped by date for display.
- DataTables endpoint returns formatted schedule list.
- Zoom integration:
  - If interview type is online and Zoom meeting is in-app, start link is used.
  - Start meeting button visibility depends on meeting creator / employee.

### 5.6.1 Admin: Application record lifecycle (delete vs archive)
Authoritative controller behaviors:
- **Archive** (`archiveJobApplication`):
  - Performs a soft delete (`$application->delete()`), moving it to the archive area.
- **Unarchive** (`unarchiveJobApplication`):
  - Restores a soft deleted application (`restore()`).
- **Hard delete** (`destroy`):
  - Deletes candidate photo file (from `candidate-photos/`) if present.
  - Calls `forceDelete()` on the application.
  - Due to DB FK rules (`ON DELETE CASCADE`), hard-deleting a job application will also remove dependent rows (answers, schedules, notes, etc.).

### 5.6.3 Admin: Candidate detail sidebar (right panel)
Controller: `AdminJobApplicationController@show`

How it is opened:
- In board/table/archive views, clicking a candidate name link (`.show-detail`) opens the right sidebar and fetches `admin.job-applications.show` via AJAX.

Authoritative UI behavior (template: `admin/job-applications/show.blade.php`):
- Left column:
  - Candidate photo (`photo_url`)
  - Resume button (visible only if `resume_url` exists)
  - Rating stars (visible only if user can `edit_job_applications`)
  - “Start Onboard” button:
    - Visible only when application status is `hired` and there is no existing onboard record.
  - Archive and Delete buttons:
    - Visible only if user can `delete_job_applications`.
- Right column (scrollable):
  - Candidate identity and contact fields
  - Applied-for job title + location
  - Optional demographic fields (gender, DOB)
  - Optional address and country/state/city block
  - Applied timestamp localized to company timezone
  - Cover letter text (if present)
  - “Additional details” section:
    - Renders each job question and either the text answer or a “View file” button.
  - Interview schedule section (if schedule exists):
    - Schedule date/time
    - Interview type (online/offline when Zoom enabled)
    - Assigned employees and their accept status badges
    - Schedule comments list (when present)
  - Skills tagging:
    - Multi-select skills list + “Add skills / Update skills” button when user can edit.
  - Applicant notes list

Behavioral notes:
- Archive button triggers soft delete (moves record to Candidate Database).
- Delete button triggers hard delete (force delete).

Interactive behaviors (authoritative JS inside `admin/job-applications/show.blade.php`):
- **Rating**:
  - Rating widget uses `jquery-bar-rating`.
  - On select it POSTs to `admin.job-applications.rating-save/{id}` with `rating`.
- **Skill tagging**:
  - Skill selection uses Select2 multi-select.
  - Saves via `POST admin.job-applications.addSkills/{applicationId}` with `skills[]`.
- **Applicant notes CRUD** (controller: `ApplicantNoteController`):
  - Add note: `POST admin.applicant-note.store` with `id` (job_application_id) and `note`.
  - Edit note: `PUT admin.applicant-note.update/{noteId}`.
  - Delete note: `DELETE admin.applicant-note.destroy/{noteId}`.
  - On success, the notes list HTML is replaced in-place.
- **Skype call button**:
  - If `skype_id` is present, Skype SDK is loaded and a call button is rendered.

### 5.6.2 Admin: Export job applications
Method: `AdminJobApplicationController@export`
- Produces an Excel XLSX file: `job-applications.xlsx`.
- Uses `JobApplicationExport` with filters:
  - `status`, `location`, `startDate`, `endDate`, `jobs`
- Includes the configured `companyName` in export context.

### 5.6.4 Admin: Candidate Database (archived applications)
Controller: `AdminApplicationArchiveController`

Purpose:
- Provides a searchable database of **archived (soft-deleted)** candidates.

List behavior (DataTables endpoint: `AdminApplicationArchiveController@data`):
- Base dataset is `JobApplication::onlyTrashed()`.
- Optional skill filter:
  - Accepts a text input (`skill`) containing a skill name substring.
  - Resolves the first matching `skills` row by: `where('name', 'LIKE', '%{skill}%')`.
  - If a matching skill is found, filters archived applications by JSON membership:
    - `whereJsonContains('skills', (string) skill_id)`.
  - If no matching skill is found, returns an empty collection.

Candidate details:
- `show(id)` returns a right-panel view for an archived application:
  - Loads application with `withTrashed()` and includes schedule/notes/onboard/status.
  - Includes job application answers.

UI behavior (template: `admin/applications-archive/index.blade.php`):
- Skill search input in the header.
- Bulk selection:
  - Header checkbox `#chkCheckAll` toggles row checkboxes.
  - Delete button is shown only when one or more rows are checked.
- Bulk delete:
  - Uses route `admin.applications-archive.deleteRecords/{rowIds}`.
  - Prompts SweetAlert confirmation.
- Export:
  - Uses route `admin.applications-archive.export/{skill}`.
  - If skill field is blank, `skill` is passed as `undefined`.
- Clicking an applicant name opens the right sidebar and loads the detail panel via AJAX.

Delete semantics:
- `deleteRecords` permanently deletes (`forceDelete`) the selected archived applications.
- Due to DB FK rules, `forceDelete` will cascade-delete related rows.

### 5.6.5 Admin: Job applications table view + documents modal + mail settings
Primary screen: `admin/job-applications/index.blade.php` (route: `admin.job-applications.table`)

Table view behavior:
- Server-side DataTables uses endpoint `admin.job-applications.data`.
- Columns:
  - Applicant name (click opens right-sidebar)
  - Job title
  - Location
  - Status (label + color pill)
  - Action buttons
- Filters:
  - Start date / end date
  - Status, company, job, location, skills (multi), question + question_value

Documents modal:
- In DataTables action column, the “View Documents” icon has class `.show-document`.
- Clicking opens a large modal (`#application-lg-modal`) with URL:
  - `admin.documents.index?documentable_type={ModelClass}&documentable_id={id}`

Export:
- Export button builds URL:
  - `admin.job-applications.export/{status}/{location}/{startDate}/{endDate}/{jobs}`
  - Empty start/end dates are substituted with `0`.

Mail settings modal:
- Triggered by `.mail_setting` button.
- Fetches current application settings via `GET admin.application-setting.create`.
- Populates checkbox list from `application_settings.mail_setting` (per status id).
- Opens modal `#ModalLoginForm` (template: `admin/application-setting/modal.blade.php`).

Company → Jobs dynamic filter:
- When company select changes, it requests `GET admin.job-applications.get-jobs?companyId={id}` and replaces the jobs dropdown options.

### 5.7 Admin: Onboarding / Offer
Controller: `AdminJobOnboardController`

Create onboarding record:
- Allowed only when candidate’s application status is `hired` (abort otherwise).
- Creates `on_board_details` record with:
  - salary, joining date, accept-last-date, report-to user, department/designation, currency, employee_status
  - generates `offer_code` (18 chars)
- Optional attachments saved to `on_board_files`.
- Optional send offer email (`send_email` flag) triggers notification `App\Notifications\JobOffer`.

Admin onboarding list UI (template: `admin/job-onboard/index.blade.php`):
- DataTables view of onboard records.
- Actions from dropdown (from controller `AdminJobOnboardController@data`):
  - View offer
  - Edit
  - Send offer (prompts confirmation and calls `admin.job-onboard.send-offer/{application_id}`)
  - Cancel offer (prompts for cancel reason and calls `admin.job-onboard.update-status/{onboard_id}`)

Send offer (method: `AdminJobOnboardController@sendOffer`):
- Ensures `on_board_details.hired_status` is `offered`.
- Sends `App\Notifications\JobOffer` to the candidate (notification channel behavior depends on notification configuration).

Cancel offer (method: `AdminJobOnboardController@updateStatus`):
- Sets:
  - `cancel_reason`
  - `hired_status = canceled`

Admin offer detail (view offer) screen:
- Template: `admin/job-onboard/show.blade.php`
- Displays:
  - Candidate contact info and applied location
  - Department/designation, salary + currency symbol, accept last date, report-to
  - Uploaded onboarding files with preview icons
  - Candidate answers to onboarding questions (`onboard_answers`)
  - Candidate signature image if accepted (`offer/sign/{sign}`)
  - Candidate rejection reason if rejected

Admin file deletion from offer detail:
- “Delete file” button triggers `DELETE admin.job-onboard.destroy/{file_id}`.
- Client-side removes the file card element on success.

### 5.8 Candidate: Offer letter response
Controller: `FrontJobOfferController@index` and `saveOffer`

Accept flow:
- Saves answers in `onboard_answers` (text or file uploads)
- Captures signature from base64 canvas:
  - Stored under `public/user-uploads/offer/sign/` when local storage
  - Stored to S3 when `filesystems.default = s3`
- Updates `on_board_details.hired_status = accepted`
- Notifies all admins: `JobOfferAccepted($jobApplication)`

Reject flow:
- Saves `reject_reason`
- Updates `hired_status = rejected`
- Notifies all admins: `JobOfferRejected($jobApplication)`

Authoritative offer UI behavior (template: `front/job-offer.blade.php`):
- Shows:
  - Candidate details
  - Job details
  - Offer details (department/designation, salary, joining date, accept last date)
  - Offer attachments (`on_board_files`) with icon previews per file extension
- If `on_board_details.hired_status == offered`:
  - Shows Accept and Reject buttons
  - Accept flow UI:
    - Displays onboarding questions form (`answer[question_id]` text/file)
    - Displays signature pad canvas (`signature_pad` JS)
    - Provides Undo and Clear buttons
    - Submit button posts to `/save-offer`
  - Reject flow UI:
    - Displays a reason textarea
    - Submit button posts to `/save-offer`
- If `hired_status == accepted`:
  - Displays stored signature image `offer/sign/{sign}`
  - Shows accepted alert
- Else (rejected/canceled):
  - Shows rejected alert

---

## 6. Authorization model (RBAC)

### 6.1 Roles and permissions
Implementation:
- Roles: `roles`
- Permissions: `permissions`
- Role-permission pivot: `permission_role`
- User-role pivot: `role_user`

Admin permission gates (pattern):
- Controllers frequently call `abort_if(!$this->user->cans('permission_name'), 403)`.

Role management UI:
- Controller: `ManageRolePermissionController`
- Supports:
  - Toggle individual permission assignment per role
  - Assign all / remove all permissions
  - Create/update/delete roles
  - Assign users to roles, detach role from user

---

## 7. Data model and database business logic

### 7.1 Database source of truth
- SQL dump reviewed: `db_hr_and_changes.sql`

### 7.2 Key tables (grouped)
Recruitment:
- `jobs`, `job_categories`, `job_locations`, `job_job_locations`, `skills`, `job_skills`, `job_types`, `work_experiences`
Applications:
- `job_applications`, `application_status`, `job_questions`, `questions`, `job_application_answers`, `documents`, `applicant_notes`
Interviews:
- `interview_schedules`, `interview_schedule_employees`, `interview_schedule_comments`, `zoom_meetings`, `zoom_settings`, `zoom_categories`, `user_zoom_meeting`
Onboarding:
- `on_board_details`, `on_board_files`, `job_offer_questions`, `job_onboard_questions`, `onboard_answers`
Settings:
- `company_settings`, `companies`, `theme_settings`, `application_settings`, `smtp_settings`, `sms_settings`, `storage_settings`, `google_captcha_settings`, `linked_in_settings`, `language_settings`, `ltm_translations`, `footer_settings`
System:
- `users`, `password_resets`, `notifications`, `migrations`, `modules`
Audit:
- `activity_logs`

### 7.3 Critical DB rules (constraints)
- The schema uses **foreign keys** widely, typically with:
  - `ON DELETE CASCADE ON UPDATE CASCADE`
- Consequence: hard-deleting a parent record (e.g., a `job_application`) cascades to child records.

### 7.4 Status/state machines
Application pipeline:
- `job_applications.status_id` → `application_status.id`
- `application_status` includes `position` and `color` for kanban ordering and UI.

Interview schedule status:
- `interview_schedules.status`: `pending | rejected | hired | canceled`

Onboarding/offer status:
- `on_board_details.hired_status`: `offered | accepted | rejected | canceled`
- `on_board_details.employee_status`: `part_time | full_time | on_contract`

### 7.6 Application status (pipeline column) management
Controller: `AdminApplicationStatusController`

Create status:
- UI is loaded from `admin.job-applications.create-status`.
- New status is inserted into `application_status` with:
  - `status` = provided name
  - `color` = provided color
  - `position` = selected position + 1
- All existing statuses with `position > selected_position` have their `position` incremented by 1.
- Also updates `application_settings.mail_setting`:
  - Adds an entry keyed by the new `status.id` with `name` and `status=true`.

Edit status:
- UI is loaded from `admin.job-applications.edit-status`.
- May change:
  - Name (`status`)
  - Color (`color`)
  - Position (`position`) with reindexing logic to keep positions sequential.

Delete status:
- If `applicationIds[]` are supplied, each referenced application is:
  - Reset to `status_id = 1`
  - Soft deleted
- Status is removed and positions of later statuses are decremented by 1.
- Also updates `application_settings.mail_setting`:
  - Removes the entry for the deleted status id.

### 7.5 JSON fields stored as TEXT
- `jobs.required_columns`, `jobs.meta_details`, `jobs.section_visibility`
- `application_settings.mail_setting`
- `activity_logs.meta` is validated as JSON in DB (`CHECK (json_valid(meta))`).

---

## 8. Integrations and external services

### 8.1 Email
- SMTP settings stored in `smtp_settings`.
- Test email route exists (`smtp-settings/sent-test-email`).

Admin UI/behavior (controller: `AdminSmtpSettingController`):
- **Settings screen**: `admin.mail-setting.index`.
- **Persisted model**: `App\EmailSetting` (this is the model used by the controller; table name aligns to `smtp_settings` in the dump).
- **Update behavior**:
  - Accepts encryption value; when UI sends string `"null"`, it is coerced to actual `null`.
  - Saves settings and then calls `verifySmtp()`.
  - If `mail_driver == 'mail'`, verification is bypassed and the update is considered successful.
  - If verification fails and host is `smtp.gmail.com`, the error message includes the “less secure apps” guidance link.
- **Send test email behavior**:
  - Validates `test_email` is a valid email.
  - Calls `verifySmtp()`.
  - On success sends notification `App\Notifications\TestEmail` to the provided email address.

### 8.2 Storage: Local vs S3
- Controlled by `filesystems.default` and `storage_settings`.
- Public asset URL generation has S3 support through `asset_url_local_s3()` / `generateS3SignedUrl()`.

Admin UI/behavior (controller: `StorageController`):
- **Settings screen**: `admin.storage-setting.index`.
- **Records**: `storage_settings` holds one row per storage type (at least `local`, optionally `aws`).
- **Local selection (`storage=local`)**:
  - Ensures `storage_settings` row with `filesystem=local` is enabled.
- **AWS selection (`storage=aws`)**:
  - Creates/updates `storage_settings` row with `filesystem=aws`.
  - Persists AWS keys as JSON string in `storage_settings.auth_keys`:
    - `driver` (hardcoded to `s3`)
    - `key`, `secret`, `region`, `bucket`
  - Marks it enabled.
- **Mutual exclusion**:
  - All other storage rows are updated to `status=disabled`.
- **Caching**:
  - Clears session cache key `storage_setting` after changing storage.

### 8.3 Zoom
- Tables: `zoom_settings`, `zoom_meetings`, and mappings.
- Interviews can be online and link to a Zoom meeting (`interview_schedules.meeting_id`).

### 8.4 Captcha
- `google_captcha_settings` stores v2/v3 keys and which pages are protected.

Admin UI/behavior (controller: `AdminSecurityController`):
- **Settings screen**: `admin.security-setting.index`.
- **Verify page**: `admin.security-setting.verify-recaptcha-v3` (route: `/security-setting/verifyCaptcha`).
- **Update behavior**:
  - Supports either version `v2` or `v3`.
  - When version is set:
    - Sets that version’s `*_site_key` and `*_secret_key`.
    - Sets that version’s status to `active` and the other version’s status to `inactive`.
  - Master `status`:
    - If requested `status=inactive`, sets both v2/v3 statuses inactive and master status inactive.
    - Otherwise sets master status active.
  - Page toggles:
    - `login_page` and `job_apply_page` are stored as `active|inactive`.

### 8.5 LinkedIn
- `linked_in_settings` control Socialite LinkedIn integration.

---

## 9. Deployment specification (shared hosting)

### 9.1 Required deployment layout
Laravel requires the **full parent project** (not only `public/`). On shared hosting:
- Upload full project outside the web root, e.g. `/home/user/meatco_hr/`
- Web root (`public_html/`) should expose the contents of `meatco_hr/public/`.
- Adjust `public_html/index.php` paths if `public/` is moved independently.

### 9.2 Writable directories
- `storage/`
- `bootstrap/cache/`

---

## 10. Appendices (to be expanded)

### Appendix A — Full route table

Note:
- This appendix is derived from `routes/web.php`.
- Routes registered via `Route::resource(...)` expand into the standard Laravel resource endpoints:
  - `index (GET)`, `create (GET)`, `store (POST)`, `show (GET)`, `edit (GET)`, `update (PUT/PATCH)`, `destroy (DELETE)`.

#### A.1 Webhook / system
- **POST** `/zoom-webhook` → `ZoomWebhookController@index` (`zoom-webhook`)

#### A.2 Public job portal (route name prefix `jobs.`; namespace `Front`)
- **GET** `/` → `FrontJobsController@jobOpenings` (`jobs.jobOpenings`) (middleware: `disable-frontend`)
- **POST** `/more-data` → `FrontJobsController@moreData` (`jobs.more-data`)
- **POST** `/search-job` → `FrontJobsController@searchJob` (`jobs.search-job`)
- **GET** `/job/{slug}/{location?}/{hash?}` → `FrontJobsController@jobDetail` (`jobs.jobDetail`)
- **GET** `/jobapply/{slug}/{location?}/{hash?}` → `FrontJobsController@jobApply` (`jobs.jobApply`)
- **POST** `/job/saveApplication` → `FrontJobsController@saveApplication` (`jobs.saveApplication`)
- **POST** `/job/fetch-country-state` → `FrontJobsController@fetchCountryState` (`jobs.fetchCountryState`)
- **POST** `/change-language/{code}` → `FrontJobsController@changeLanguage` (`jobs.changeLanguage`)
- **GET** `/auth/redirect/{provider}` → `FrontJobsController@redirect` (`jobs.linkedinRedirect`)
- **GET** `/auth/callback/{provider}` → `FrontJobsController@callback` (`jobs.linkedinCallback`)
- **GET** `/job-alert` → `FrontJobsController@jobAlert` (`jobs.jobAlert`)
- **POST** `/save-job-alert` → `FrontJobsController@saveJobAlert` (`jobs.saveJobAlert`)
- **POST** `/disable-job-alert/{id}` → `FrontJobsController@disableJobAlert` (`jobs.disableJobAlert`)
- **GET** `/job-offer/{slug?}` → `FrontJobOfferController@index` (`jobs.job-offer`)
- **POST** `/save-offer` → `FrontJobOfferController@saveOffer` (`jobs.save-offer`)
- **GET** `/{slug}` → `FrontJobsController@customPage` (`jobs.custom-page`)

#### A.3 Auth scaffold
- `Auth::routes()` (login, logout, password reset, etc.)

#### A.4 Authenticated (middleware `auth`)
Notifications:
- **POST** `/mark-notification-read` → `NotificationController@markAllRead` (`mark-notification-read`)
- **POST** `/mark-read` → `NotificationController@markRead` (`mark_single_notification_read`)

##### A.4.1 Admin (prefix `/admin`, name prefix `admin.`, namespace `Admin`)
Dashboard:
- **GET** `/admin/dashboard` → `AdminDashboardController@index` (`admin.dashboard`)

Job categories:
- **GET** `/admin/job-categories/data` → `AdminJobCategoryController@data` (`admin.job-categories.data`)
- **GET** `/admin/job-categories/getSkills/{categoryId}` → `AdminJobCategoryController@getSkills` (`admin.job-categories.getSkills`)
- **RESOURCE** `/admin/job-categories` → `AdminJobCategoryController`

Questions:
- **GET** `/admin/questions/data` → `AdminQuestionController@data` (`admin.questions.data`)
- **RESOURCE** `/admin/questions` → `AdminQuestionController`

Todo:
- **POST** `/admin/todo-items/update-todo-item` → `AdminTodoItemController@updateTodoItem` (`admin.todo-items.updateTodoItem`)
- **RESOURCE** `/admin/todo-items` → `AdminTodoItemController`

Job alerts (admin):
- **GET** `/admin/job_alert/data` → `AdminJobAlertController@data` (`admin.job_alert.data`)
- **RESOURCE** `/admin/job_alert` → `AdminJobAlertController` (only: `index`, `destroy`)

Activity logs:
- **GET** `/admin/logs` → `ActivityLogController@index` (`admin.logs.index`)
- **GET** `/admin/logs/data` → `ActivityLogController@data` (`admin.logs.data`)

Settings group (prefix `/admin/settings`):
- **RESOURCE** `/admin/settings/settings` → `CompanySettingsController` (only: `edit`, `update`, `index`)
- **RESOURCE** `/admin/settings/application-setting` → `ApplicationSettingsController`
- **RESOURCE** `/admin/settings/role-permission` → `ManageRolePermissionController`
  - **POST** `/admin/settings/role-permission/assignAllPermission` → `assignAllPermission`
  - **POST** `/admin/settings/role-permission/removeAllPermission` → `removeAllPermission`
  - **POST** `/admin/settings/role-permission/assignRole` → `assignRole`
  - **POST** `/admin/settings/role-permission/detachRole` → `detachRole`
  - **POST** `/admin/settings/role-permission/storeRole` → `storeRole`
  - **POST** `/admin/settings/role-permission/deleteRole` → `deleteRole`
  - **GET** `/admin/settings/role-permission/showMembers/{id}` → `showMembers`
- **RESOURCE** `/admin/settings/language-settings` → `LanguageSettingsController`
  - **POST** `/admin/settings/language-settings/change-language` → `changeLanguage`
  - **PUT** `/admin/settings/language-settings/change-language-status/{id}` → `changeStatus`
- **RESOURCE** `/admin/settings/theme-settings` → `AdminThemeSettingsController`
  - **POST** `/admin/settings/theme-settings/disable-frontend` → `disableFrontend`
- **RESOURCE** `/admin/settings/smtp-settings` → `AdminSmtpSettingController`
  - **GET** `/admin/settings/smtp-settings/sent-test-email` → `sendTestEmail`
- **RESOURCE** `/admin/settings/sms-settings` → `AdminSmsSettingsController` (only: `index`, `update`)
- **RESOURCE** `/admin/settings/linkedin-settings` → `AdminLinkedInSettingsController`
- **RESOURCE** `/admin/settings/footer-settings` → `FooterSettingController`
  - **GET** `/admin/settings/footer-settings/data` → `data`
- **GET** `/admin/settings/update-application` → `UpdateApplicationController@index`

Zoom:
- **RESOURCE** `/admin/zoom-meeting` → `AdminZoomMeetingController`
  - **GET** `/admin/zoom-meeting/table` → `tableView`
  - **GET** `/admin/zoom-meeting/data` → `data`
  - **GET** `/admin/zoom-meeting/start-meeting/{id}` → `startMeeting`
  - **POST** `/admin/zoom-meeting/cancel-meeting` → `cancelMeeting`
  - **POST** `/admin/zoom-meeting/end-meeting` → `endMeeting`
  - **POST** `/admin/zoom-meeting/updateOccurrence/{id}` → `updateOccurrence`
- **RESOURCE** `/admin/category` → `CategoryController`
- **RESOURCE** `/admin/zoom-setting` → `ZoomMeetingSettingController`
  - **POST** `/admin/zoom-setting/change-status/{id}` → `changeStatus`

Master data:
- **RESOURCE** `/admin/skills` → `AdminSkillsController` (+ `GET /admin/skills/data`)
- **RESOURCE** `/admin/locations` → `AdminLocationsController` (+ `GET /admin/locations/data`)
- **RESOURCE** `/admin/departments` → `AdminDepartmentController`
- **RESOURCE** `/admin/designations` → `AdminDesignationController`
- **RESOURCE** `/admin/work-experience` → `AdminWorkExperienceController`
- **RESOURCE** `/admin/job-type` → `AdminJobTypeController`
- **RESOURCE** `/admin/company` → `AdminCompanyController` (+ `GET /admin/company/data`)
- **RESOURCE** `/admin/currency-settings` → `AdminCurrencyController`

Jobs:
- **RESOURCE** `/admin/jobs` → `AdminJobsController` (+ `GET /admin/jobs/data`)
  - **POST** `/admin/jobs/refresh-date` → `refreshDate`
  - **GET** `/admin/jobs/application-data` → `applicationData`
  - **GET** `/admin/jobs/send-email` → `sendEmail`
  - **POST** `/admin/jobs/send-emails` → `sendEmails`

Applications:
- **RESOURCE** `/admin/job-applications` → `AdminJobApplicationController`
  - **POST** `/admin/job-applications/rating-save/{id?}` → `ratingSave`
  - **GET** `/admin/job-applications/create-schedule/{id?}` → `createSchedule`
  - **POST** `/admin/job-applications/store-schedule` → `storeSchedule`
  - **GET** `/admin/job-applications/question/{jobID}/{applicationId?}` → `jobQuestion`
  - **GET** `/admin/job-applications/export/{status}/{location}/{startDate}/{endDate}/{jobs}` → `export`
  - **GET** `/admin/job-applications/data` → `data`
  - **GET** `/admin/job-applications/load-more` → `loadMore`
  - **GET** `/admin/job-applications/table-view` → `table`
  - **POST** `/admin/job-applications/updateIndex` → `updateIndex`
  - **POST** `/admin/job-applications/archive-job-application/{application}` → `archiveJobApplication`
  - **POST** `/admin/job-applications/unarchive-job-application/{application}` → `unarchiveJobApplication`
  - **POST** `/admin/job-applications/add-skills/{applicationId}` → `addSkills`
  - **GET** `/admin/job-applications/get-jobs` → `getJobs`

- **RESOURCE** `/admin/applications-archive` → `AdminApplicationArchiveController`
  - **GET** `/admin/applications-archive/data` → `data`
  - **GET** `/admin/applications-archive/export/{skill}` → `export`
  - **POST** `/admin/applications-archive{id}` → `deleteRecords`
    - Note: as written, this route is missing a slash before `{id}`.

Onboarding:
- **RESOURCE** `/admin/job-onboard` → `AdminJobOnboardController`
  - **GET** `/admin/job-onboard/data` → `data`
  - **GET** `/admin/job-onboard/send-offer/{id?}` → `sendOffer`
  - **GET** `/admin/job-onboard/update-status/{id?}` → `updateStatus`
- **RESOURCE** `/admin/job-onboard-questions` → `AdminJobOfferQuestionController` (+ `GET /admin/job-onboard-questions/data`)

Application status (pipeline columns):
- **RESOURCE** `/admin/application-status` → `AdminApplicationStatusController`

Interview schedule:
- **RESOURCE** `/admin/interview-schedule` → `InterviewScheduleController`
  - **GET** `/admin/interview-schedule/data` → `data`
  - **GET** `/admin/interview-schedule/table-view` → `table`
  - **POST** `/admin/interview-schedule/change-status` → `changeStatus`
  - **POST** `/admin/interview-schedule/change-status-multiple` → `changeStatusMultiple`
  - **GET** `/admin/interview-schedule/notify/{id}/{type}` → `notify`
  - **GET** `/admin/interview-schedule/response/{id}/{type}` → `employeeResponse`

Team:
- **RESOURCE** `/admin/team` → `AdminTeamController`
  - **GET** `/admin/team/data` → `data`
  - **POST** `/admin/team/change-role` → `changeRole`

Security / storage:
- **RESOURCE** `/admin/security-setting` → `AdminSecurityController` (only: `index`, `update`)
- **GET** `/admin/security-setting/verifyCaptcha` → `AdminSecurityController@verifyCaptcha`
- **RESOURCE** `/admin/storage-settings` → `StorageController`

Documents:
- **RESOURCE** `/admin/documents` → `AdminDocumentController`
  - **GET** `/admin/documents/data` → `data`
  - **GET** `/admin/documents/download-document/{document}` → `downloadDoc`

Sticky notes / applicant notes:
- **RESOURCE** `/admin/sticky-note` → `AdminStickyNotesController`
- **RESOURCE** `/admin/applicant-note` → `ApplicantNoteController`

Reports:
- **RESOURCE** `/admin/report` → `AdminReportController`

##### A.4.2 Phone verification
- **GET** `/change-mobile` → `VerifyMobileController@changeMobile`
- **POST** `/send-otp-code` → `VerifyMobileController@sendVerificationCode`
- **POST** `/send-otp-code/account` → `VerifyMobileController@sendVerificationCode`
- **POST** `/verify-otp-phone` → `VerifyMobileController@verifyOtpCode`
- **POST** `/verify-otp-phone/account` → `VerifyMobileController@verifyOtpCode`
- **GET** `/remove-session` → `VerifyMobileController@removeSession`

### Appendix B — ERD summary

This is a **relationship-focused summary** of the schema from `db_hr_and_changes.sql`.

#### B.1 Recruitment master data
- `companies (1) -> (N) jobs`
  - `jobs.company_id` FK → `companies.id`
- `jobs (N) <-> (N) job_locations` via `job_job_locations`
  - `job_job_locations.job_id` → `jobs.id`
  - `job_job_locations.location_id` → `job_locations.id`
- `jobs (N) <-> (N) skills` via `job_skills`
- `jobs (N) <-> (N) questions` via `job_questions`
- `job_categories (1) -> (N) jobs` (category foreign key)
- `work_experiences (1) -> (N) jobs`
- `job_types (1) -> (N) jobs`

#### B.2 Job applications
- `jobs (1) -> (N) job_applications`
  - `job_applications.job_id` → `jobs.id`
- `job_locations (1) -> (N) job_applications`
  - `job_applications.location_id` → `job_locations.id`
- `application_status (1) -> (N) job_applications`
  - `job_applications.status_id` → `application_status.id`
- `job_applications (1) -> (N) job_application_answers`
  - `job_application_answers.job_application_id` → `job_applications.id`
- `questions (1) -> (N) job_application_answers`
  - `job_application_answers.question_id` → `questions.id`

Notes and documents:
- `job_applications (1) -> (N) applicant_notes`
  - `applicant_notes.job_application_id` → `job_applications.id`
- `documents` is polymorphic:
  - `documents.documentable_type` and `documents.documentable_id`
  - Used for resume uploads and potentially other attachments.

#### B.3 Interview scheduling
- `job_applications (1) -> (N) interview_schedules`
- `interview_schedules (N) <-> (N) users` via `interview_schedule_employees`
- `interview_schedules (1) -> (N) interview_schedule_comments`
- `interview_schedules (N) -> (1) zoom_meetings` (when online)

#### B.4 Onboarding / offers
- `job_applications (1) -> (0..1) on_board_details`
- `on_board_details (1) -> (N) on_board_files`
- `on_board_details (1) -> (N) onboard_answers`
- `job_onboard_questions (1) -> (N) onboard_answers`

#### B.5 Authorization (RBAC)
- `users (N) <-> (N) roles` via `role_user`
- `roles (N) <-> (N) permissions` via `permission_role`

#### B.6 Cascade-delete invariants (important)
- Many child tables declare `ON DELETE CASCADE`.
- A `forceDelete()` of `job_applications` will cascade-delete:
  - `job_application_answers`
  - `applicant_notes`
  - `interview_schedules` and dependent schedule employee/comment rows
  - `on_board_details` and dependent offer files/answers

### Appendix C — Configuration matrix

This system is configured through a mix of:
- `.env` keys (runtime config)
- DB tables for settings (admin-editable)

#### C.1 Database connectivity
- **.env**:
  - `DB_CONNECTION`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`

#### C.2 Mail
- **DB table**: `smtp_settings` (edited via Admin SMTP Settings)
  - driver, host, port, username, password, encryption, from_name, from_email
- **Behavior**: `AdminSmtpSettingController@sendTestEmail` verifies SMTP then sends a TestEmail notification.

#### C.3 Storage (local vs S3)
- **DB table**: `storage_settings`
  - Determines which filesystem is enabled and stores AWS keys as JSON (`auth_keys`).
- **.env typical keys when S3 is active**:
  - `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, `AWS_BUCKET` (Laravel filesystem)
- **Note**: Actual app behavior can also rely on DB-stored JSON keys for S3.

#### C.4 Zoom
- **DB tables**: `zoom_settings`, `zoom_categories`
- **.env typical keys** (depending on `macsidigital/laravel-zoom` setup):
  - Zoom API key/secret (exact key names depend on package configuration)

#### C.5 Google reCAPTCHA
- **DB table**: `google_captcha_settings`
  - v2 and v3 keys + page toggles
  - master enable/disable
- **Impacted UI**:
  - Admin login (when enabled)
  - Front job apply page (when enabled)

#### C.6 LinkedIn (Socialite)
- **DB table**: `linked_in_settings`
  - `client_id`, `client_secret`, `status`
- **Also requires** correct Socialite redirect URL routing:
  - `/auth/redirect/linkedin` and `/auth/callback/linkedin`

#### C.7 Multi-language
- **DB table**: `language_settings`
- **DB table**: `ltm_translations` (translation manager)

#### C.8 Theme + UI customization
- **DB table**: `theme_settings`
  - primary color, custom CSS (admin + front), welcome text, background image
- **DB table**: `footer_settings`
  - custom front pages content + slug

