Skip to content

Epic 5 — Job Board


BE-JOB-001 — Extend Job schema with full spec fields

  • [x] Implemented

Files:

  • Edit: prisma/schema.prisma
  • Edit: src/graphql/schema/index.ts — expand Job, CreateJobInput, UpdateJobInput, JobFilterInput

New fields to add:

prisma
projectName     String?
ageMin          Int?
ageMax          Int?
gender          String[]   @default([])
languages       Json?      // [{ language, fluency, accent }]
experienceLevel String?
auditionRequired Boolean   @default(false)
paymentCurrency String     @default("USD")
locationCity    String?
locationCountry String?
unionRequired   String?

Expand JobFilterInput: add genre, ageMin/Max, language, location, commitmentType, experienceLevel, unionStatus filter fields.


BE-JOB-002 — Job Application model & pipeline

  • [x] Implemented

Files:

  • Edit: prisma/schema.prisma — add Application model
  • Run: pnpm db:migrate
  • Edit: src/graphql/schema/index.ts — add Application type, applyToJob mutation, myApplications query, jobApplications(jobId) query (producer only)
  • Create: src/graphql/resolvers/application.ts
  • Edit: src/graphql/resolvers/index.ts
  • Create: src/services/job/application.ts

Schema:

prisma
model Application {
  id         String   @id @default(cuid())
  jobId      String
  job        Job      @relation(fields: [jobId], references: [id], onDelete: Cascade)
  talentId   String
  talent     TalentProfile @relation(fields: [talentId], references: [id], onDelete: Cascade)
  status     ApplicationStatus @default(PENDING)
  coverNote  String?
  reelUrl    String?
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  @@unique([jobId, talentId])
  @@map("applications")
}
enum ApplicationStatus {
  PENDING
  REVIEWED
  SHORTLISTED
  REJECTED
  HIRED
}

Mutations: applyToJob(jobId, coverNote?, reelUrl?), updateApplicationStatus(id, status) (producer)
Queries: myApplications: [Application!]! (talent), jobApplications(jobId: ID!): [Application!]! (producer)


BE-JOB-003 — Producer swipe actions (shortlist / skip / block)

  • [x] Implemented

Files:

  • Edit: prisma/schema.prisma — add TalentInteraction model
  • Edit: src/graphql/schema/index.ts — add mutations
  • Create: src/graphql/resolvers/interaction.ts
  • Create: src/services/job/interactions.ts

Schema:

prisma
model TalentInteraction {
  id           String   @id @default(cuid())
  producerId   String
  talentId     String
  jobId        String?
  type         InteractionType
  expiresAt    DateTime?
  createdAt    DateTime @default(now())
  @@unique([producerId, talentId, jobId])
  @@map("talent_interactions")
}
enum InteractionType {
  SHORTLISTED
  SKIPPED
  BLOCKED
}

Mutations: swipeTalent(talentId, jobId, action: SHORTLIST | SKIP | BLOCK): Boolean!
Blocked interactions expire after 90 days (set expiresAt). Discover feed excludes talents with active interactions.


BE-JOB-004 — Job posting missing spec fields

  • [x] Implemented

Files:

  • Edit: prisma/schema.prisma — add columns to Job and JobRequirements
  • Run: pnpm db:migrate
  • Edit: src/graphql/schema/index.ts — extend Job, CreateJobInput, UpdateJobInput
  • Edit: src/graphql/resolvers/job.ts
  • Edit: src/__tests__/resolvers/job.test.ts

Fields to add:

prisma
// On Job model
rehearsalStartDate   DateTime?
rehearsalEndDate     DateTime?
responseTimeEstimate String?      // e.g. "Within 48 hours"
contactPerson        String?      // name of casting agent if different from producer
contactPersonEmail   String?
contractType         String?      // FREELANCE | FULL_TIME | PART_TIME | TEMPORARY | ONE_TIME | RETAINER
auditionCount        Int?         // how many rounds of audition if auditionRequired = true
requiredMaterials    String[]     @default([])  // e.g. ["headshot", "demo reel", "cover letter"]
otherPhysicalReqs    String?      // freetext for additional physical requirements

Notes:

  • contractType is more granular than the existing CommitmentType enum — both fields coexist.
  • requiredMaterials drives what the talent sees on the application form (show/hide fields conditionally).
  • rehearsalStartDate/rehearsalEndDate are separate from startDate/endDate (production dates).

FE-JOBS-001 — Job listings page (talent view)

  • [x] Implemented — superseded by FE-JOBS-006 (role-dispatched /jobs + match grid)

Files:

  • Edit: apps/app/src/pages/jobs/JobsPage.tsx — replace placeholder
  • Create: apps/app/src/lib/queries/jobs.tsJOBS_QUERY, JOB_QUERY
  • Create: apps/app/src/hooks/useJobs.ts
  • Create: apps/app/src/hooks/useJob.ts

Description: Replace "Jobs board coming soon" with a real list using JobCard (DS). Shows filter bar at top (production type, remote, payment type). Infinite scroll with React Query useInfiniteQuery. Shows Skeleton during load. Shows EmptyState when no results. Matches "Listagem de vagas" frame in Figma.

Superseded: the /jobs route is now role-dispatched (see FE-JOBS-006). The talent view becomes TalentJobsPage, backed by the new jobsForTalent query (BE-JOB-005) which returns a per-job match score. The original JobsPage.tsx is removed.


FE-JOBS-002 — Job detail page (talent view)

  • [x] Implemented

Files:

  • Create: apps/app/src/pages/jobs/JobDetailPage.tsx
  • Edit: apps/app/src/App.tsx — add route /jobs/:id

Description: Full job detail view. Sections: header (title, producer, production type), compensation, commitment, requirements, description, location, deadline. Apply button triggers applyToJob mutation with optional cover note. Uses DS components. Matches "Detalhes" screens in Figma.


FE-JOBS-003 — Job posting form (producer)

  • [x] Implemented

Files:

  • Create: apps/app/src/pages/jobs/CreateJobPage.tsx
  • Create: apps/app/src/pages/jobs/steps/JobBasicStep.tsx
  • Create: apps/app/src/pages/jobs/steps/JobRequirementsStep.tsx
  • Create: apps/app/src/pages/jobs/steps/JobCompensationStep.tsx
  • Create: apps/app/src/pages/jobs/steps/JobReviewStep.tsx
  • Create: apps/app/src/hooks/useCreateJob.ts
  • Edit: apps/app/src/App.tsx — add route /jobs/create (producer-only)

Description: Multi-step form to post a job. Step 1: Basic info (title, project name, production type, genre). Step 2: Requirements (talent category, skills, experience level, age range, gender, languages, work authorization, union). Step 3: Compensation & logistics (payment type/amount, commitment, location/remote, start/end dates, deadline). Step 4: Review & publish. All DS components.


FE-JOBS-004 — Producer job management dashboard

  • [x] Implemented — superseded by FE-JOBS-007 (producer view moves to /jobs as a table)

Files:

  • Create: apps/app/src/pages/jobs/ProducerJobsPage.tsx
  • Create: apps/app/src/pages/jobs/JobApplicationsPage.tsx — list applicants for a job
  • Create: apps/app/src/hooks/useProducerJobs.ts
  • Create: apps/app/src/hooks/useJobApplications.ts

Description: Producer view of their posted jobs with status badges (OPEN/CLOSED/DRAFT), applicant count, deadline. Tap a job → see applicants list. Each applicant shows TalentCard (compact) with status chip and action buttons (Shortlist / Reject / Hire). Producer can update application status here.

Superseded: the producer view moves from /jobs/manage to /jobs (role-dispatched). The grid is replaced by a DS data table — see FE-JOBS-007. /jobs/manage becomes a redirect.


FE-JOBS-005 — Extend job detail & posting form for new spec fields

  • [x] Implemented

Files:

  • Edit: apps/app/src/pages/jobs/JobDetailPage.tsx — show rehearsal dates, required materials, contact person, physical reqs, contract type
  • Edit: apps/app/src/pages/jobs/steps/JobBasicStep.tsx — add contract type, contact person, contact email
  • Edit: apps/app/src/pages/jobs/steps/JobCompensationStep.tsx — add rehearsal start/end date pickers, response time estimate
  • Edit: apps/app/src/pages/jobs/steps/JobRequirementsStep.tsx — add required materials checklist, other physical requirements field, audition count input
  • Edit: apps/app/src/lib/queries/jobs.ts — add new fields to JOB_QUERY

Job detail view additions:

  • Rehearsal dates (if set) shown alongside production dates
  • Required application materials shown as a checklist (so talent knows what to prepare before applying)
  • Contact person name/email if different from the producer
  • Other physical requirements as a note block
  • Contract type badge alongside commitment type

Job posting form additions:

  • JobBasicStep: contract type dropdown (Freelance / Full-Time / Part-Time / Temporary / One-time / Retainer), contact person name + email fields
  • JobRequirementsStep: audition count input (shown when auditionRequired = true), required materials MultiSelect (Headshot / Demo Reel / Cover Letter / References / Portfolio / Showreel), other physical requirements textarea
  • JobCompensationStep: rehearsal start/end date pickers, response time estimate input (free text: "Within 48 hours")

BE-JOB-005 — Talent job match feed (jobsForTalent)

  • [x] Implemented

Files:

  • Edit: src/graphql/schema/index.ts — add JobMatchFeedItem, JobMatchConnection, jobsForTalent query
  • Edit: src/graphql/resolvers/job.tsjobsForTalent resolver
  • Create: src/services/ai/jobMatching.ts — talent-vs-job scoring (mirror of discoverFeed's talent scoring, inverted)
  • Edit: src/__tests__/resolvers/job.test.ts

Description: Talent-facing analogue of discoverFeed. Returns a paginated, scored list of open jobs ranked for the calling talent. Uses the same 9-factor model from BE-DISCOVER-001 with the perspective inverted (rank jobs for one talent, instead of talents for one job). Excludes jobs the talent has already applied to.

GraphQL:

graphql
type JobMatchFeedItem {
  job: Job!
  score: Float!          # 0-1
  matchReasons: [String!]!
}

type JobMatchConnection {
  edges: [JobMatchFeedItem!]!
  pageInfo: PageInfo!
}

extend type Query {
  jobsForTalent(first: Int, after: String, filter: JobFilterInput): JobMatchConnection!
}

Notes:

  • Default first = 10 (matches the frontend grid page size).
  • Cursor pagination — same convention as the existing jobs(...) resolver.
  • Resolver enforces that the caller has an active talent profile; errors otherwise.

BE-PETJOB-001 — PetJob model + CRUD

  • [x] Implemented

Files:

  • Edit: prisma/schema.prisma — new PetJob model
  • Run: pnpm db:migrate
  • Edit: src/graphql/schema/index.tsPetJob type, CreatePetJobInput, UpdatePetJobInput, PetJobFilterInput, PetJobConnection
  • Create: src/graphql/resolvers/petJob.ts
  • Create: src/services/petJob/index.ts
  • Edit: src/graphql/resolvers/index.ts
  • Edit: src/__tests__/resolvers/petJob.test.ts

Schema (initial — mirror of Job plus pet-specific requirement fields):

prisma
model PetJob {
  id                String   @id @default(cuid())
  producerId        String
  producer          ProducerProfile @relation(fields: [producerId], references: [id], onDelete: Cascade)
  title             String
  productionType    String?
  productionSubtype String?
  genre             String?
  description       String?
  // Pet-specific requirements
  species           String[]   @default([])   // dog, cat, horse, etc.
  breeds            String[]   @default([])
  ageMonthsMin      Int?
  ageMonthsMax      Int?
  trainedSkills     String[]   @default([])   // sit, stay, fetch, agility, on-leash, etc.
  experienceLevel   String?                   // novice | intermediate | professional
  coatColors        String[]   @default([])
  sizeRange         String?                   // small | medium | large | xl
  vaccinationRequired Boolean  @default(false)
  // Logistics (mirror of Job)
  paymentType       PaymentType
  paymentAmount     Float?
  paymentCurrency   String     @default("USD")
  location          String?
  isRemote          Boolean    @default(false)
  startDate         DateTime?
  endDate           DateTime?
  applicationDeadline DateTime?
  status            JobStatus  @default(OPEN)
  createdAt         DateTime   @default(now())
  updatedAt         DateTime   @updatedAt
  applications      PetJobApplication[]
  @@map("pet_jobs")
}

Mutations: createPetJob, updatePetJob, closePetJob
Queries: petJob(id), petJobs(first, after, filter)

Notes:

  • PetJobApplication (apply with a specific pet) is a follow-up — out of scope for this ticket. Tracked separately when the apply flow is built.
  • PaymentType and JobStatus enums are reused from the existing Job model.

BE-PETJOB-002 — Pet-owner pet-job match feed (petJobsForOwner)

  • [x] Implemented

Files:

  • Edit: src/graphql/schema/index.ts — add PetJobMatchFeedItem, PetJobMatchConnection, petJobsForOwner query
  • Edit: src/graphql/resolvers/petJob.ts — resolver
  • Create: src/services/ai/petJobMatching.ts — scoring service
  • Edit: src/__tests__/resolvers/petJob.test.ts

Description: Pet-owner-facing match feed. Scores PetJobs against the pet-owner's pets (best-pet-match score per job) and returns a paginated, ranked list.

Scoring factors:

  1. Species match (hard filter — score 0 if no pet matches species)
  2. Breed match (any owned pet)
  3. Age window match
  4. Trained-skills overlap
  5. Size match
  6. Vaccination compliance (when required)
  7. Availability vs startDate
  8. Location proximity / remote

GraphQL:

graphql
type PetJobMatchFeedItem {
  petJob: PetJob!
  score: Float!
  matchReasons: [String!]!
  bestMatchingPetId: ID    # which of the owner's pets scored highest
}

type PetJobMatchConnection {
  edges: [PetJobMatchFeedItem!]!
  pageInfo: PageInfo!
}

extend type Query {
  petJobsForOwner(first: Int, after: String, filter: PetJobFilterInput): PetJobMatchConnection!
}

BE-JOB-006 — jobListing union for /jobs/:id

  • [x] Implemented

Files:

  • Edit: src/graphql/schema/index.ts — add JobListing union
  • Edit: src/graphql/resolvers/job.tsjobListing resolver
  • Edit: src/__tests__/resolvers/job.test.ts

GraphQL:

graphql
union JobListing = Job | PetJob

extend type Query {
  jobListing(id: ID!): JobListing
}

Description: Single resolver for the role+type-aware /jobs/:id page. Looks up the ID in Job first, then PetJob. Returns null if neither match (frontend renders 404). Frontend dispatches by __typename and active profile.

Notes:

  • Existing job(id) and petJob(id) queries stay — admin and internal callers use them directly.
  • IDs across Job and PetJob are CUIDs so the namespace risk is negligible, but the resolver order (Job → PetJob) is deterministic.

FE-JOBS-006 — Role-dispatched /jobs + Talent match grid

  • [x] Implemented — supersedes FE-JOBS-001

Files:

  • Create: apps/app/src/pages/jobs/JobsRouter.tsx — dispatches by active profile type
  • Create: apps/app/src/pages/jobs/TalentJobsPage.tsx — infinite-scroll grid with match %
  • Create: apps/app/src/hooks/useJobsForTalent.tsuseInfiniteQuery, 10/page
  • Edit: apps/app/src/lib/queries/jobs.ts — add JOBS_FOR_TALENT_QUERY + JobMatchFeedItem fragment
  • Delete: apps/app/src/pages/jobs/JobsPage.tsx
  • Edit: apps/app/src/App.tsx/jobs<JobsRouter />; /jobs/manage<Navigate to="/jobs" replace />
  • Edit: packages/design-system/src/components/JobCard.tsx — accept optional matchScore + matchReasons (rendered as a progress ring or chip)

Description: /jobs is now role-aware. For talent profiles, it renders TalentJobsPage: a responsive grid (1/2/3 cols) of JobCards with the per-job match percentage shown prominently. First page is 10 items via useInfiniteQuery, additional pages loaded on scroll using IntersectionObserver. Filter bar (production type, remote, payment type) above the grid persists query state in the URL.

Empty / loading / error states: DS Skeleton grid during initial load; DS EmptyState when no jobs match the filter; toast on network error.


FE-JOBS-007 — Producer jobs table at /jobs

  • [x] Implemented — supersedes FE-JOBS-004

Files:

  • Create: apps/app/src/pages/jobs/ProducerJobsPage.tsx (rewrite — was list, now table)
  • Edit: apps/app/src/hooks/useProducerJobs.ts — page/pageSize args (server-side pagination)
  • Edit: apps/app/src/lib/queries/jobs.tsMY_JOBS_QUERY with pageInfo
  • Edit: apps/app/src/App.tsx — drop /jobs/manage (route folded into JobsRouter)
  • Add: packages/design-system/src/components/JobsTable.tsx if not already in DS

Description: When a producer hits /jobs, JobsRouter renders this table. Columns: Title, Status badge, Applications count, Casting submissions count, Posted, Deadline, Actions (... menu: View, Edit, Close, Generate flier, View castings). Default sort: most recent first. Pagination controls below the table per pagination rule. Row click → /jobs/:id (producer view per FE-JOBS-009).


FE-JOBS-008 — Pet-owner match grid at /jobs

  • [x] Implemented

Files:

  • Create: apps/app/src/pages/jobs/PetOwnerJobsPage.tsx
  • Create: apps/app/src/hooks/usePetJobsForOwner.ts
  • Edit: apps/app/src/lib/queries/petJobs.ts (new file) — PET_JOBS_FOR_OWNER_QUERY + fragment
  • Edit: packages/design-system/src/components/JobCard.tsx — add PetJobCard variant (or pass kind: 'pet') showing species/breed instead of skills

Description: Pet-owner variant of TalentJobsPage. Same grid + infinite-scroll layout, but driven by petJobsForOwner and shows pet-specific match reasons (e.g., "Bella matches the breed and trained skills"). When the owner has multiple pets, the card highlights the best-matching pet by name.


FE-JOBS-009 — Role+type-aware /jobs/:id detail

  • [x] Implemented — single JobListingPage dispatches by union __typename; role-aware behavior lives inside JobDetailPage / PetJobDetailPage rather than separate view subcomponents.

Files:

  • Edit: apps/app/src/pages/jobs/JobDetailPage.tsx — fetch via jobListing(id) union
  • Create: apps/app/src/pages/jobs/views/TalentJobView.tsx — apply flow (extracted from current JobDetailPage)
  • Create: apps/app/src/pages/jobs/views/ProducerJobView.tsx — castings tab, applicants tab, manage actions
  • Create: apps/app/src/pages/jobs/views/PetOwnerPetJobView.tsx — apply-with-pet flow
  • Edit: apps/app/src/lib/queries/jobs.tsJOB_LISTING_QUERY returning the union with __typename

Description: Single page resolves the listing via the union, then dispatches by __typename (Job vs PetJob) and active profile (producer / talent / pet-owner). Each view is a self-contained subcomponent — no role logic intermixed at the page level beyond the dispatcher.

Dispatch matrix:

ProfileJobPetJob
Producer (owner)ProducerJobViewProducerPetJobView (future)
Producer (other)Read-only TalentJobViewRead-only view
TalentTalentJobView (apply)404 (talent can't apply to pet jobs)
Pet owner404PetOwnerPetJobView (apply with pet)

Open follow-ups: ProducerPetJobView is deferred until BE-PETJOB-001 + an application model for pet jobs lands.


BE-JOB-007 — Job wizard schema extension

  • [x] Implemented

Files:

  • Edit: prisma/schema.prismaJob model gains the wizard fields
  • Run: pnpm db:migrate20260601220244_add_job_wizard_fields
  • Edit: src/graphql/schema/index.tsJob, CreateJobInput, UpdateJobInput; new WorkMode enum
  • Edit: src/graphql/validation/index.ts — Zod tightened
  • Edit: src/graphql/resolvers/job.tswithWorkModeMirror keeps the legacy isRemote filter in sync

Fields added to Job: projectName, talentCategory, auditionRequired, accentsDialects[], materialsRequired[], applicationInstructions, contactPersonName, contactPersonEmail, contractType, salaryMin, salaryMax, workMode (IN_PERSON | REMOTE | HYBRID), recordingDates Json.


BE-JOB-008 — Job ownership enforcement on updateJob / deleteJob

  • [x] Implemented

Files:

  • Edit: src/graphql/resolvers/job.ts — fetch the producer profile from ctx.user.sub, compare to job.producerId, throw FORBIDDEN otherwise
  • Edit: src/__tests__/resolvers/job.test.ts — three new ownership cases (success, non-owner, missing job)

Before this fix, any producer could update or delete any job. PetJob mutations already had the check; this brings Jobs to parity.


BE-JOB-009 — Application contact + submitted materials

  • [x] Implemented

Files:

  • Edit: prisma/schema.prismaApplication gains applicantEmail, applicantPhone, submittedMaterials Json
  • Run: pnpm db:migrate20260602004355_add_application_contact_materials
  • Edit: src/graphql/schema/index.ts — extend Application type, add ApplicationMaterialInput, applyToJob accepts the new variables
  • Edit: src/graphql/validation/index.ts — Zod for new fields incl. email + URL validation
  • Edit: src/graphql/resolvers/job.tsapplyToJob writes the new columns

submittedMaterials shape: Array<{ material: string; mediaItemId?: string; uploadedUrl?: string }> — one entry per Job.materialsRequired slot, pointing either to a portfolio item or a fresh upload.


BE-PETJOB-003 — PetJob wizard schema parity

  • [x] Implemented

Files:

  • Edit: prisma/schema.prismaPetJob gains the same wizard fields as Job (minus the talent-specific ones)
  • Run: pnpm db:migrate20260601231109_add_pet_job_wizard_fields
  • Edit: src/graphql/schema/index.tsPetJob, CreatePetJobInput, UpdatePetJobInput
  • Edit: src/graphql/resolvers/petJob.tswithPetJobWorkModeMirror mirrors the Job pattern

Fields added: projectName, auditionRequired, salaryMin, salaryMax, contractType, workMode, recordingDates, materialsRequired[], applicationInstructions, contactPersonName, contactPersonEmail. talentCategory and accentsDialects intentionally omitted (don't apply to pets).


FE-JOBS-010 — Multi-stage job creation wizard (6 stages)

  • [x] Implemented

Files:

  • Rewrite: apps/app/src/pages/jobs/JobCreatePage.tsx as a 6-stage wizard
  • Edit: apps/app/src/hooks/useProducerJobs.ts — extend CreateJobInput
  • Edit: apps/app/src/lib/queries/jobs.ts — extend JOB_FRAGMENT
  • Edit: apps/app/src/hooks/useJobs.tsJob TS type + WorkMode
  • Edit: packages/i18n/locales/{en,pt,es}/app.json — full jobCreate.* block

Stages: Basic information → Production details → Job description → Specific requirements → Logistics & Compensation → Application process. Reuses Card, Chip, Input, Select, Textarea, MultiSelect from @castyou/design-system. Header shows a thin progress bar + STAGE n of 6 / <name> + Next: <next> preview. Free-form ChipInput for genre / skills / accents / materials; three-button workMode selector; salary min/max pair; materials suggestions; structured-casting toggle preserved on Stage 6.


FE-JOBS-011 — Job edit page (reuse wizard)

  • [x] Implemented

Files:

  • Edit: apps/app/src/pages/jobs/JobCreatePage.tsx — accept { mode, initialData, jobId } props
  • Create: apps/app/src/pages/jobs/JobEditPage.tsx — fetches via useJob, maps Job → CreateJobInput, renders the wizard in edit mode
  • Edit: apps/app/src/App.tsx/jobs/:id/edit route
  • Edit: apps/app/src/pages/jobs/ProducerJobsPage.tsx — Edit row action on JobsTab

In edit mode, the header reads "Edit job", the submit button reads "Save changes", and the structured-casting toggle is hidden (casting map has its own dedicated /jobs/:id/casting-map/edit page).


FE-JOBS-012 — Apply-to-job 3-stage wizard

  • [x] Implemented

Files:

  • Create: apps/app/src/pages/jobs/JobApplyPage.tsx
  • Edit: apps/app/src/lib/queries/jobs.tsAPPLY_TO_JOB_MUTATION accepts new variables; new GET_ME_FOR_APPLY query for prefill
  • Edit: apps/app/src/hooks/useJob.tsuseApplyToJob accepts an ApplyInput object
  • Edit: apps/app/src/App.tsx/jobs/:id/apply route
  • Edit: apps/app/src/pages/jobs/JobDetailPage.tsx — Apply Now navigates to the wizard (replaces the modal)
  • Edit: packages/i18n/locales/{en,pt,es}/app.jsonjobApply.* block

Stages: Contact info + library/upload → Match each Job.materialsRequired slot to a chosen media item → Review + Send. Uses the existing GET_MEDIA_UPLOAD_URL upload pipeline for fresh files; ties them to the Application.submittedMaterials JSON column via uploadedUrl, or attaches existing portfolio items via mediaItemId.


FE-JOBS-013 — Inline applicants panel on job detail

  • [x] Implemented

Files:

  • Edit: apps/app/src/pages/jobs/JobDetailPage.tsx — new ProducerApplicantsPanel component, rendered when activeProfile === 'PRODUCER' | 'AGENCY'
  • Edit: apps/app/src/pages/jobs/PetJobDetailPage.tsx — same panel pattern for pet jobs

Backend's jobApplications(jobId) resolver enforces ownership; non-owners get FORBIDDEN and the panel hides itself silently. Each row shows avatar → name → primaryCategory → cover note → status badge → per-row status Select. The panel also surfaces applicantEmail / applicantPhone (as mailto: / tel: links) and the submittedMaterials list with links to uploaded files or "From portfolio" / "Not provided" indicators.


FE-JOBS-014 — Route-based Jobs / Pet jobs tabs

  • [x] Implemented

Files:

  • Edit: apps/app/src/pages/jobs/ProducerJobsPage.tsx — tab state moved from useState to useSearchParams

The Pet jobs tab encodes as ?tab=pets; Jobs is the default (no param). Clicking a tab pushes a history entry, so browser Back from a detail page returns to the previously active tab.


FE-JOBS-015 — Flier thumbnails in producer jobs list

  • [x] Implemented

Files:

  • Edit: apps/app/src/pages/jobs/ProducerJobsPage.tsxThumbnail component layered fallback: flierUrl image → FlierThumbnail canvas → empty placeholder
  • Pet jobs table gets the same thumbnail column

The AI-generated flierSpec renders as a tiny canvas via the existing FlierThumbnail component. Empty placeholders for rows without either field.


FE-PETJOB-001 — Pet job creation + edit wizards

  • [x] Implemented

Files:

  • Create: apps/app/src/pages/jobs/PetJobCreatePage.tsx — 6-stage wizard with pet-specific stages
  • Create: apps/app/src/pages/jobs/PetJobEditPage.tsx
  • Edit: apps/app/src/hooks/useProducerJobs.tsuseCreatePetJob, useUpdatePetJob hooks + CreatePetJobInput
  • Edit: apps/app/src/lib/queries/petJobs.tsPET_JOB_FRAGMENT, CREATE_PET_JOB_MUTATION, UPDATE_PET_JOB_MUTATION
  • Edit: apps/app/src/hooks/usePetJobsForOwner.tsPetJob TS type extended
  • Edit: apps/app/src/App.tsx/pet-jobs/create and /pet-jobs/:id/edit routes
  • Edit: apps/app/src/pages/jobs/ProducerJobsPage.tsx — header "Post job" button switches to "Post pet job" when the Pet jobs tab is active; Edit row action on PetJobsTab
  • Edit: packages/i18n/locales/{en,pt,es}/app.jsonpetJobCreate.* block

Pet-specific stages: Stage 3 "Pet description" — species multi-select, breeds chips, trained skills chips, novice/intermediate/professional experience levels. Stage 4 "Pet requirements" — age in months, size range (small/medium/large/xl), coat colors, vaccination required toggle, audition required toggle, number of roles.


FE-PETJOB-002 — PetJobDetailPage parity with JobDetailPage

  • [x] Implemented

Files:

  • Rewrite: apps/app/src/pages/jobs/PetJobDetailPage.tsx

Same purple gradient hero, info-pill row (Payment / Work model / Openings), flyer section, structured bullet sections (Pet requirements, Required trained skills), Apply gradient pill button (wired to ApplyWithPetModal), ProducerCard at the bottom, and an inline ProducerApplicantsPanel for the job owner (wrapped in the same surface-card styling).