{"path":"apps/dashboard/src/pages/job-detail.tsx","size":18751,"locked":false,"binary":false,"truncated":false,"content":"import { useState } from \"react\";\nimport { useParams, Link } from \"wouter\";\nimport { ChevronLeft, ExternalLink, Plus, Trash2, CheckCircle2, XCircle, AlertTriangle, FileCode2, Globe } from \"lucide-react\";\nimport {\n  useGetJob,\n  useSubmitDeliverable,\n  useUpdateJob,\n  getGetJobQueryKey,\n} from \"@workspace/api-client-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { StatusBadge } from \"@/components/status-badge\";\nimport { formatLamports, truncatePubkey, formatDate } from \"@/lib/format\";\n\ntype DeliverableType = \"url\" | \"ipfs\" | \"text\" | \"json\";\n\nconst INPUT =\n  \"w-full px-4 py-3 text-sm font-mono glass rounded-xl text-white/80 placeholder:text-white/20 outline-none focus:border-emerald-500/40 focus:shadow-[0_0_16px_rgba(20,241,149,0.06)] transition-all duration-200 min-h-[44px] bg-transparent\";\n\nconst DELIVERABLE_ICONS: Record<DeliverableType, React.ElementType> = {\n  url: Globe,\n  ipfs: FileCode2,\n  text: FileCode2,\n  json: FileCode2,\n};\n\nfunction OutcomeBadge({ outcome }: { outcome: string | null }) {\n  if (!outcome) return null;\n\n  const config: Record<string, { icon: React.ElementType; color: string; glow: string; desc: string }> = {\n    RELEASE: {\n      icon: CheckCircle2,\n      color: \"text-emerald-400 border-emerald-500/25 bg-emerald-500/[0.06]\",\n      glow: \"shadow-[0_0_40px_rgba(20,241,149,0.12)]\",\n      desc: \"Payment released to freelancer\",\n    },\n    REFUND: {\n      icon: XCircle,\n      color: \"text-amber-400 border-amber-500/25 bg-amber-500/[0.06]\",\n      glow: \"shadow-[0_0_40px_rgba(251,191,36,0.1)]\",\n      desc: \"Funds returned to client\",\n    },\n    ESCALATE: {\n      icon: AlertTriangle,\n      color: \"text-purple-400 border-purple-500/25 bg-purple-500/[0.06]\",\n      glow: \"shadow-[0_0_40px_rgba(153,69,255,0.1)]\",\n      desc: \"Models diverged — human arbitration required\",\n    },\n  };\n\n  const c = config[outcome];\n  if (!c) return null;\n  const Icon = c.icon;\n\n  return (\n    <div className={`glass rounded-2xl px-6 py-8 text-center border ${c.color} ${c.glow}`}>\n      <Icon className=\"w-8 h-8 mx-auto mb-3 opacity-80\" />\n      <div className=\"text-[9px] text-white/30 uppercase tracking-widest mb-2 font-mono\">Oracle Verdict</div>\n      <div className=\"text-2xl font-bold font-mono\">{outcome}</div>\n      <div className=\"text-xs mt-2 opacity-50 font-mono\">{c.desc}</div>\n    </div>\n  );\n}\n\nfunction GlassSection({ title, children }: { title: string; children: React.ReactNode }) {\n  return (\n    <div className=\"glass rounded-2xl overflow-hidden\">\n      <div className=\"px-5 py-3.5 border-b border-white/[0.05] bg-white/[0.02]\">\n        <span className=\"text-[9px] font-mono text-white/30 uppercase tracking-[0.15em]\">{title}</span>\n      </div>\n      {children}\n    </div>\n  );\n}\n\nfunction InfoRow({\n  label,\n  value,\n  link,\n}: {\n  label: string;\n  value: React.ReactNode;\n  link?: string;\n}) {\n  return (\n    <div className=\"flex flex-col sm:flex-row sm:items-start gap-1.5 sm:gap-6 py-3.5 px-5 border-b border-white/[0.04] last:border-b-0 hover:bg-white/[0.02] transition-colors\">\n      <span className=\"text-[9px] font-mono text-white/25 uppercase tracking-widest shrink-0 sm:w-24 sm:pt-0.5\">\n        {label}\n      </span>\n      <span className=\"text-xs font-mono text-white/60 break-all flex items-start gap-1.5\">\n        {link ? (\n          <a\n            href={link}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"text-emerald-400/70 hover:text-emerald-400 flex items-center gap-1.5 transition-colors\"\n          >\n            {value}\n            <ExternalLink className=\"w-3 h-3 shrink-0\" />\n          </a>\n        ) : (\n          value\n        )}\n      </span>\n    </div>\n  );\n}\n\nexport default function JobDetail() {\n  const { jobId } = useParams<{ jobId: string }>();\n  const qc = useQueryClient();\n\n  const { data: job, isLoading, error } = useGetJob(jobId ?? \"\", {\n    query: { enabled: !!jobId, queryKey: [\"job\", jobId] },\n  });\n\n  const submitDeliverable = useSubmitDeliverable();\n  const updateJob = useUpdateJob();\n\n  const [deliverableType, setDeliverableType] = useState<DeliverableType>(\"url\");\n  const [deliverable, setDeliverable] = useState(\"\");\n  const [desc, setDesc] = useState(\"\");\n  const [criteria, setCriteria] = useState<string[]>([\"\"]);\n  const [outcome, setOutcome] = useState<string | null>(null);\n  const [submitError, setSubmitError] = useState<string | null>(null);\n  const [submitted, setSubmitted] = useState(false);\n\n  const addCriterion = () => setCriteria((c) => [...c, \"\"]);\n  const removeCriterion = (i: number) =>\n    setCriteria((c) => c.filter((_, idx) => idx !== i));\n  const setCriterion = (i: number, val: string) =>\n    setCriteria((c) => c.map((v, idx) => (idx === i ? val : v)));\n\n  const invalidate = () =>\n    qc.invalidateQueries({ queryKey: getGetJobQueryKey(jobId ?? \"\") });\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    setSubmitError(null);\n    setOutcome(null);\n\n    submitDeliverable.mutate(\n      {\n        jobId: jobId ?? \"\",\n        data: {\n          description: desc || job?.description || \"\",\n          acceptanceCriteria: criteria.filter((c) => c.trim() !== \"\"),\n          deliverable,\n          deliverableType,\n        },\n      },\n      {\n        onSuccess: (result) => {\n          setOutcome(result.outcome ?? null);\n          setSubmitted(true);\n          invalidate();\n        },\n        onError: (err) =>\n          setSubmitError(err instanceof Error ? err.message : \"Submission failed.\"),\n      }\n    );\n  };\n\n  if (isLoading) {\n    return (\n      <div className=\"flex-1 p-5 md:p-8 space-y-4\">\n        {Array.from({ length: 5 }).map((_, i) => (\n          <div key={i} className={`glass rounded-2xl shimmer`} style={{ height: `${64 + i * 8}px` }} />\n        ))}\n      </div>\n    );\n  }\n\n  if (error || !job) {\n    return (\n      <div className=\"flex-1 flex flex-col items-center justify-center p-8 text-center\">\n        <div className=\"glass rounded-2xl px-8 py-12 max-w-sm\">\n          <p className=\"text-white/30 text-sm font-mono mb-4\">Job not found.</p>\n          <Link href=\"/jobs\" className=\"text-emerald-400 text-xs font-mono hover:text-emerald-300 transition-colors inline-flex items-center gap-1\">\n            <ChevronLeft className=\"w-3 h-3\" /> Back to all jobs\n          </Link>\n        </div>\n      </div>\n    );\n  }\n\n  const parsedCriteria: string[] = (() => {\n    if (!job.acceptanceCriteria) return [];\n    try {\n      return JSON.parse(job.acceptanceCriteria) as string[];\n    } catch {\n      return [];\n    }\n  })();\n\n  return (\n    <div className=\"flex-1 flex flex-col\">\n      {/* Header */}\n      <div className=\"relative px-5 md:px-8 py-6 md:py-7 border-b border-white/[0.05] overflow-hidden\">\n        <div className=\"absolute inset-0 bg-gradient-to-r from-emerald-500/[0.02] via-transparent to-purple-500/[0.02]\" />\n        <div className=\"relative\">\n          <Link\n            href=\"/jobs\"\n            className=\"flex items-center gap-1.5 text-[10px] font-mono text-white/25 hover:text-white/60 mb-3 w-fit transition-colors\"\n          >\n            <ChevronLeft className=\"w-3 h-3\" /> All Jobs\n          </Link>\n          <div className=\"flex flex-col sm:flex-row sm:items-center gap-2.5\">\n            <h1 className=\"text-2xl md:text-3xl font-bold tracking-tight text-white font-mono\">{job.jobId}</h1>\n            <StatusBadge status={job.status} />\n          </div>\n          <p className=\"text-[10px] text-white/25 font-mono mt-2\">\n            Created {formatDate(job.createdAt)} · Updated {formatDate(job.updatedAt)}\n          </p>\n        </div>\n      </div>\n\n      <div className=\"flex-1 overflow-y-auto p-5 md:p-8\">\n        <div className=\"max-w-3xl mx-auto space-y-5\">\n\n          {/* Action buttons */}\n          {job.status === \"pending\" && (\n            <div className=\"flex flex-col sm:flex-row gap-3\">\n              <button\n                onClick={() =>\n                  updateJob.mutate(\n                    { jobId: jobId ?? \"\", data: { status: \"completed\" } },\n                    { onSuccess: invalidate }\n                  )\n                }\n                disabled={updateJob.isPending}\n                className=\"flex-1 py-3 px-5 text-xs font-mono font-bold rounded-xl glass text-emerald-400 border-emerald-500/25 hover:bg-emerald-500/10 hover:border-emerald-500/40 hover:shadow-[0_0_20px_rgba(20,241,149,0.1)] active:scale-[0.97] disabled:opacity-50 transition-all duration-200 min-h-[48px]\"\n              >\n                Mark Complete\n              </button>\n              <button\n                onClick={() =>\n                  updateJob.mutate(\n                    { jobId: jobId ?? \"\", data: { status: \"cancelled\" } },\n                    { onSuccess: invalidate }\n                  )\n                }\n                disabled={updateJob.isPending}\n                className=\"flex-1 py-3 px-5 text-xs font-mono font-bold rounded-xl glass text-rose-400 border-rose-500/25 hover:bg-rose-500/10 hover:border-rose-500/40 active:scale-[0.97] disabled:opacity-50 transition-all duration-200 min-h-[48px]\"\n              >\n                Cancel Job\n              </button>\n            </div>\n          )}\n\n          {/* Verdict */}\n          {outcome && <OutcomeBadge outcome={outcome} />}\n\n          {/* Escrow details */}\n          <GlassSection title=\"Escrow Details\">\n            <div>\n              <InfoRow label=\"Job ID\" value={job.jobId} />\n              <InfoRow label=\"Amount\" value={<span className=\"text-emerald-400\">◎ {formatLamports(job.amountLamports)} SOL</span>} />\n              <InfoRow\n                label=\"Client\"\n                value={\n                  <span className=\"flex flex-col gap-0.5\">\n                    <span className=\"text-white/70\">{truncatePubkey(job.clientPubkey)}</span>\n                    <span className=\"text-[9px] text-white/25 break-all hidden sm:block\">{job.clientPubkey}</span>\n                  </span>\n                }\n              />\n              <InfoRow\n                label=\"Freelancer\"\n                value={\n                  <span className=\"flex flex-col gap-0.5\">\n                    <span className=\"text-white/70\">{truncatePubkey(job.freelancerPubkey)}</span>\n                    <span className=\"text-[9px] text-white/25 break-all hidden sm:block\">{job.freelancerPubkey}</span>\n                  </span>\n                }\n              />\n              <InfoRow label=\"Oracle\" value={truncatePubkey(job.oraclePubkey)} />\n              {job.txSig && (\n                <InfoRow\n                  label=\"Tx Sig\"\n                  value={truncatePubkey(job.txSig)}\n                  link={`https://explorer.solana.com/tx/${job.txSig}?cluster=devnet`}\n                />\n              )}\n            </div>\n          </GlassSection>\n\n          {/* Description */}\n          {job.description && (\n            <GlassSection title=\"Description\">\n              <p className=\"px-5 py-4 text-sm text-white/60 leading-relaxed\">{job.description}</p>\n            </GlassSection>\n          )}\n\n          {/* Acceptance criteria */}\n          {parsedCriteria.length > 0 && (\n            <GlassSection title=\"Acceptance Criteria\">\n              <ul className=\"px-5 py-4 space-y-3\">\n                {parsedCriteria.map((c, i) => (\n                  <li key={i} className=\"text-sm font-mono flex gap-3 items-start text-white/60\">\n                    <span className=\"text-emerald-400/50 shrink-0 mt-0.5 font-bold\">›</span>\n                    <span>{c}</span>\n                  </li>\n                ))}\n              </ul>\n            </GlassSection>\n          )}\n\n          {/* Submit deliverable */}\n          {job.status === \"pending\" && !submitted && (\n            <GlassSection title=\"Submit Deliverable\">\n              <form onSubmit={handleSubmit} className=\"p-5 space-y-5\">\n                {/* Type selector */}\n                <div className=\"flex flex-col gap-2\">\n                  <label className=\"text-[9px] font-mono text-white/25 uppercase tracking-[0.15em]\">Deliverable Type</label>\n                  <div className=\"flex gap-2 flex-wrap\">\n                    {([\"url\", \"ipfs\", \"text\", \"json\"] as DeliverableType[]).map((t) => {\n                      const Icon = DELIVERABLE_ICONS[t];\n                      return (\n                        <button\n                          key={t}\n                          type=\"button\"\n                          onClick={() => setDeliverableType(t)}\n                          className={`flex items-center gap-1.5 px-3.5 py-2 text-[10px] font-mono rounded-full border transition-all duration-200 ${\n                            deliverableType === t\n                              ? \"bg-emerald-500/15 text-emerald-400 border-emerald-500/30 shadow-[0_0_12px_rgba(20,241,149,0.1)]\"\n                              : \"glass text-white/30 border-white/[0.07] hover:text-white/60 hover:border-white/15\"\n                          }`}\n                        >\n                          <Icon className=\"w-3 h-3\" />\n                          {t}\n                        </button>\n                      );\n                    })}\n                  </div>\n                </div>\n\n                <div className=\"flex flex-col gap-2\">\n                  <label className=\"text-[9px] font-mono text-white/25 uppercase tracking-[0.15em]\">\n                    {deliverableType === \"url\" && \"Deliverable URL\"}\n                    {deliverableType === \"ipfs\" && \"IPFS CID / Gateway URL\"}\n                    {deliverableType === \"text\" && \"Deliverable Text\"}\n                    {deliverableType === \"json\" && \"JSON Payload\"}\n                  </label>\n                  <textarea\n                    className={`${INPUT} resize-none`}\n                    rows={4}\n                    placeholder={\n                      deliverableType === \"url\"\n                        ? \"https://github.com/…\"\n                        : deliverableType === \"ipfs\"\n                        ? \"Qm…\"\n                        : deliverableType === \"json\"\n                        ? '{ \"repo\": \"…\", \"commit\": \"…\" }'\n                        : \"Describe what was delivered…\"\n                    }\n                    value={deliverable}\n                    onChange={(e) => setDeliverable(e.target.value)}\n                    required\n                  />\n                </div>\n\n                <div className=\"flex flex-col gap-2\">\n                  <label className=\"text-[9px] font-mono text-white/25 uppercase tracking-[0.15em]\">Description (for oracle)</label>\n                  <textarea\n                    className={`${INPUT} resize-none`}\n                    rows={2}\n                    placeholder={job.description ?? \"Describe the deliverable…\"}\n                    value={desc}\n                    onChange={(e) => setDesc(e.target.value)}\n                  />\n                </div>\n\n                <div className=\"flex flex-col gap-2\">\n                  <label className=\"text-[9px] font-mono text-white/25 uppercase tracking-[0.15em]\">Acceptance Criteria</label>\n                  <div className=\"space-y-2.5\">\n                    {criteria.map((c, i) => (\n                      <div key={i} className=\"flex gap-2\">\n                        <input\n                          className={`${INPUT} flex-1`}\n                          placeholder={parsedCriteria[i] ?? `Criterion ${i + 1}…`}\n                          value={c}\n                          onChange={(e) => setCriterion(i, e.target.value)}\n                        />\n                        {criteria.length > 1 && (\n                          <button\n                            type=\"button\"\n                            onClick={() => removeCriterion(i)}\n                            className=\"w-11 h-11 flex items-center justify-center glass rounded-xl text-rose-400/50 hover:text-rose-400 hover:border-rose-400/30 active:scale-95 transition-all duration-200 shrink-0\"\n                          >\n                            <Trash2 className=\"w-3.5 h-3.5\" />\n                          </button>\n                        )}\n                      </div>\n                    ))}\n                    <button\n                      type=\"button\"\n                      onClick={addCriterion}\n                      className=\"flex items-center gap-1.5 text-xs font-mono text-white/25 hover:text-emerald-400 transition-colors py-1\"\n                    >\n                      <Plus className=\"w-3.5 h-3.5\" />\n                      Add criterion\n                    </button>\n                  </div>\n                </div>\n\n                {submitError && (\n                  <div className=\"glass rounded-xl px-4 py-3 text-xs font-mono text-rose-400 border-rose-500/20 bg-rose-500/[0.05]\">\n                    {submitError}\n                  </div>\n                )}\n\n                <div className=\"border-t border-white/[0.05] pt-5 space-y-4\">\n                  <p className=\"text-[10px] font-mono text-white/25 leading-relaxed\">\n                    Sends this deliverable to the Gemini + Claude dual-model consensus engine.\n                    Both models must independently agree above threshold to release or refund escrow.\n                  </p>\n                  <button\n                    type=\"submit\"\n                    disabled={submitDeliverable.isPending}\n                    className=\"w-full py-3.5 text-xs font-mono font-bold rounded-xl bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:from-emerald-400 hover:to-emerald-300 active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-[0_0_24px_rgba(20,241,149,0.2)] hover:shadow-[0_0_32px_rgba(20,241,149,0.35)] min-h-[48px]\"\n                  >\n                    {submitDeliverable.isPending\n                      ? \"Verifying via Oracle…\"\n                      : \"Submit for AI Verification\"}\n                  </button>\n                </div>\n              </form>\n            </GlassSection>\n          )}\n\n          {submitted && outcome && (\n            <button\n              onClick={() => {\n                setSubmitted(false);\n                setOutcome(null);\n                setDeliverable(\"\");\n              }}\n              className=\"text-xs font-mono text-white/25 hover:text-white/60 transition-colors flex items-center gap-1.5\"\n            >\n              <ChevronLeft className=\"w-3 h-3\" /> Submit another deliverable\n            </button>\n          )}\n\n          {job.status !== \"pending\" && (\n            <div className=\"glass rounded-2xl px-6 py-8 text-center font-mono text-sm text-white/25\">\n              This job is <StatusBadge status={job.status} /> — no further deliverables can be submitted.\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"}