Thoroughly explore the RateRight codebase to document the CONTRACTOR user flow. I need to understand every screen, component, API call, and data collection point for contractors.

Search for:
1. Contractor signup flow - look in src/app/(auth)/, src/app/signup/, or similar
2. Company profile creation / contractor onboarding
3. Job posting screens - how contractors create jobs
4. Worker browsing / matching - how contractors find workers
5. Hire flow - how contractors select and hire workers
6. Payment/Stripe integration - where the $50 fee is charged
7. Contact details reveal - when/how worker contact info is shown
8. Contractor dashboard
9. Messaging flow after hire
10. Job completion and rating

For each screen/component found, note:
- The file path and route
- What data is collected or displayed
- What Supabase/API calls are made
- Payment integration details (Stripe)
- Any guards or conditions

Search broadly across: src/app/, src/components/, src/lib/, app/, components/, lib/
Also check for any API routes in src/app/api/ or app/api/

Be very thorough - check all directories and naming conventions.


I'll systematically explore the RateRight codebase to document the contractor user flow. Let me start by understanding the directory structure and then trace through the contractor journey.


Bash

find /home/ccuser/the-50-dollar-app -type f -name "*.tsx" -o -name "*.ts" -o -name "*.json" | head -50


Let me explore the actual source structure:


ls -la /home/ccuser/the-50-dollar-app/src/ 2>/dev/null || ls -la /home/ccuser/the-50-dollar-app/


total 32
drwxrwxr-x  7 ccuser ccuser 4096 Feb  9 07:00 .
drwxrwxr-x 12 ccuser ccuser 4096 Feb  9 16:21 ..
drwxrwxr-x  8 ccuser ccuser 4096 Feb  9 14:46 app
drwxrwxr-x  4 ccuser ccuser 4096 Feb  9 07:00 components
drwxrwxr-x  2 ccuser ccuser 4096 Feb  9 07:00 hooks
drwxr-xr-x  2 root   root   4096 Feb  9 04:52 hooks-old
drwxrwxr-x  4 ccuser ccuser 4096 Feb  9 07:01 lib
-rw-rw-r--  1 ccuser ccuser  336 Feb  9 04:05 proxy.ts


Good, it's a Next.js App Router structure. Let me explore the app directory structure and then search systematically:


find /home/ccuser/the-50-dollar-app/app -type d | head -30


find: ‘/home/ccuser/the-50-dollar-app/app’: No such file or directory


find /home/ccuser/the-50-dollar-app -maxdepth 3 -type d -name "*auth*" -o -name "*signup*" -o -name "*contractor*" 2>/dev/null


Now let me explore the directory structure comprehensively:


tree -L 3 /home/ccuser/the-50-dollar-app/src/app -I 'node_modules' 2>/dev/null || find /home/ccuser/the-50-dollar-app/src/app -maxdepth 3 -type f -name "*.tsx" -o -name "*.ts" | sort


Now let me search for contractor-specific files and understand the structure better:


find /home/ccuser/the-50-dollar-app -type f \( -name "*.tsx" -o -name "*.ts" \) ! -path "*/node_modules/*" ! -path "*/.next*" | grep -E "(contractor|job|payment|hire|match|worker|company)" | sort


Excellent! Now let me systematically read through all the contractor flow files. Let me start with the key pages:


1→"use client";
     2→import { useState } from "react";
     3→import { useRouter } from "next/navigation";
     4→import Link from "next/link";
     5→import { createClient } from "@/lib/supabase/client";
     6→import { Button } from "@/components/ui/button";
     7→import { Input } from "@/components/ui/input";
     8→import { Label } from "@/components/ui/label";
     9→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
    10→import { HardHat, Building2 } from "lucide-react";
    11→import type { UserType } from "@/lib/types";
    12→
    13→export default function SignupPage() {
    14→  const [email, setEmail] = useState("");
    15→  const [password, setPassword] = useState("");
    16→  const [userType, setUserType] = useState<UserType | null>(null);
    17→  const [agreedToTerms, setAgreedToTerms] = useState(false);
    18→  const [error, setError] = useState<string | null>(null);
    19→  const [loading, setLoading] = useState(false);
    20→  const router = useRouter();
    21→  const supabase = createClient();
    22→
    23→  async function handleSignup(e: React.FormEvent) {
    24→    e.preventDefault();
    25→    if (!userType) {
    26→      setError("Please select whether you're hiring or looking for work");
    27→      return;
    28→    }
    29→    if (!agreedToTerms) {
    30→      setError("Please agree to the Terms of Service and Privacy Policy");
    31→      return;
    32→    }
    33→
    34→    setLoading(true);
    35→    setError(null);
    36→
    37→    const { error } = await supabase.auth.signUp({
    38→      email,
    39→      password,
    40→      options: {
    41→        data: { type: userType },
    42→        emailRedirectTo: `${window.location.origin}/auth/callback`,
    43→      },
    44→    });
    45→
    46→    if (error) {
    47→      setError(error.message);
    48→      setLoading(false);
    49→      return;
    50→    }
    51→
    52→    // Redirect to email verification page instead of dashboard
    53→    router.push("/auth/verify-email");
    54→    router.refresh();
    55→  }
    56→
    57→  return (
    58→

    59→      <Card className="w-full max-w-md">
    60→        <CardHeader className="text-center">
    61→          <CardTitle className="text-2xl font-bold">Create your account</CardTitle>
    62→          <CardDescription>
    63→            Sign up to find work or hire workers
    64→          </CardDescription>
    65→        </CardHeader>
    66→        <CardContent>
    67→          <form onSubmit={handleSignup} className="space-y-4">
    68→

    69→

    70→                Email
    71→                <Input
    72→                  id="email"
    73→                  type="email"
    74→                  value={email}
    75→                  onChange={(e) => setEmail(e.target.value)}
    76→                  required
    77→                  placeholder="you@example.com"
    78→                  className="h-12 text-base"
    79→                />
    80→

    81→

    82→                Password
    83→                <Input
    84→                  id="password"
    85→                  type="password"
    86→                  value={password}
    87→                  onChange={(e) => setPassword(e.target.value)}
    88→                  required
    89→                  placeholder="Create a password"
    90→                  minLength={6}
    91→                  className="h-12 text-base"
    92→                />
    93→

    94→

    95→
    96→

    97→              I am a...
    98→

    99→                <Button
   100→                  type="button"
   101→                  variant={userType === "contractor" ? "default" : "outline"}
   102→                  onClick={() => setUserType("contractor")}
   103→                  className="h-24 flex-col gap-2"
   104→                >
   105→                  <Building2 className="h-6 w-6" />
   106→                  Contractor
   107→                  Hiring workers
   108→                </Button>
   109→                <Button
   110→                  type="button"
   111→                  variant={userType === "worker" ? "default" : "outline"}
   112→                  onClick={() => setUserType("worker")}
   113→                  className="h-24 flex-col gap-2"
   114→                >
   115→                  <HardHat className="h-6 w-6" />
   116→                  Worker
   117→                  Looking for work
   118→                </Button>
   119→

   120→

   121→
   122→

   123→              <input
   124→                type="checkbox"
   125→                id="terms"
   126→                checked={agreedToTerms}
   127→                onChange={(e) => setAgreedToTerms(e.target.checked)}
   128→                className="mt-1 h-4 w-4 rounded border-border accent-primary"
   129→              />
   130→
   131→                I agree to the{" "}
   132→                <Link href="/legal/terms-of-service" className="text-primary underline hover:no-underline" target="_blank">
   133→                  Terms of Service
   134→                </Link>{" "}
   135→                and{" "}
   136→                <Link href="/legal/privacy-policy" className="text-primary underline hover:no-underline" target="_blank">
   137→                  Privacy Policy
   138→                </Link>
   139→
   140→

   141→
   142→            {error && (
   143→

   144→
{error}

   145→

   146→            )}
   147→
   148→            <Button type="submit" className="w-full h-12 text-base font-semibold" disabled={loading || !agreedToTerms}>
   149→              {loading ? "Creating account..." : "Create account"}
   150→            </Button>
   151→
   152→

   153→              Already have an account?{" "}
   154→              <Link href="/auth/login" className="text-primary underline hover:no-underline">
   155→                Sign in
   156→              </Link>
   157→

   158→          </form>
   159→        </CardContent>
   160→      </Card>
   161→

   162→  );
   163→}
   164→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import { logger } from "@/lib/logger";
     6→import Link from "next/link";
     7→import Image from "next/image";
     8→import toast from "react-hot-toast";
     9→import { createClient } from "@/lib/supabase/client";
    10→import { Button } from "@/components/ui/button";
    11→import { Input } from "@/components/ui/input";
    12→import { Label } from "@/components/ui/label";
    13→import { Card, CardContent } from "@/components/ui/card";
    14→import {
    15→  Building2,
    16→  Search,
    17→  Sparkles,
    18→  CheckCircle2,
    19→  ArrowLeft,
    20→  ArrowRight,
    21→  Loader2,
    22→  Edit3,
    23→  Globe,
    24→  MapPin,
    25→  Users,
    26→  FileText,
    27→} from "lucide-react";
    28→import { guessCompanyDomain } from "@/lib/company-utils";
    29→import { validateABN, formatABN } from "@/lib/abn-utils";
    30→
    31→type Step = "auth" | "company" | "researching" | "review";
    32→
    33→interface CompanyData {
    34→  name: string;
    35→  abn: string;
    36→  address: string;
    37→  industry: string;
    38→  size: string;
    39→  website: string;
    40→  logoUrl?: string | null;
    41→  logoSource?: 'clearbit' | 'google' | 'none';
    42→}
    43→
    44→async function lookupABN(abn: string): Promise<CompanyData> {
    45→  // Clean the ABN - remove spaces
    46→  const cleanAbn = abn.replace(/\s/g, '');
    47→
    48→  // Call the ABN lookup API
    49→  const response = await fetch(`/api/abn-lookup?abn=${cleanAbn}`);
    50→
    51→  if (!response.ok) {
    52→    const errData = await response.json().catch(() => null);
    53→    throw new Error(errData?.error || 'Failed to lookup ABN');
    54→  }
    55→
    56→  const result = await response.json();
    57→
    58→  if (!result.success || !result.data) {
    59→    throw new Error(result.error || 'Unable to lookup ABN');
    60→  }
    61→
    62→  const data = result.data;
    63→
    64→  // Map the ABR data to our CompanyData format
    65→  const companyData: CompanyData = {
    66→    name: data.name || 'Unknown Company',
    67→    abn: formatABN(data.abn || cleanAbn),
    68→    address: [data.postcode, data.state].filter(Boolean).join(' ') || 'Address not available',
    69→    industry: 'Construction',
    70→    size: 'Unknown',
    71→    website: '',
    72→  };
    73→
    74→  // Try to find company logo
    75→  await fetchCompanyLogo(companyData);
    76→
    77→  return companyData;
    78→}
    79→
    80→async function fetchCompanyLogo(companyData: CompanyData): Promise<void> {
    81→  try {
    82→    // If we have an explicit website, try that first
    83→    if (companyData.website) {
    84→      const domain = extractDomainFromUrl(companyData.website);
    85→      if (domain) {
    86→        const logoResponse = await fetch(`/api/company-logo?domain=${domain}`);
    87→        if (logoResponse.ok) {
    88→          const logoData = await logoResponse.json();
    89→          companyData.logoUrl = logoData.logoUrl;
    90→          companyData.logoSource = logoData.source;
    91→          return;
    92→        }
    93→      }
    94→    }
    95→
    96→    // Otherwise, try to guess domains from company name
    97→    const possibleDomains = guessCompanyDomain(companyData.name);
    98→
    99→    // Try each domain until we find a logo
   100→    for (const domain of possibleDomains) {
   101→      const logoResponse = await fetch(`/api/company-logo?domain=${domain}`);
   102→      if (logoResponse.ok) {
   103→        const logoData = await logoResponse.json();
   104→        if (logoData.logoUrl) {
   105→          companyData.logoUrl = logoData.logoUrl;
   106→          companyData.logoSource = logoData.source;
   107→          return;
   108→        }
   109→      }
   110→    }
   111→
   112→    // No logo found
   113→    companyData.logoUrl = null;
   114→    companyData.logoSource = 'none';
   115→  } catch (error) {
   116→    logger.error('Logo fetch failed:', error);
   117→    companyData.logoUrl = null;
   118→    companyData.logoSource = 'none';
   119→  }
   120→}
   121→
   122→function extractDomainFromUrl(url: string): string | null {
   123→  try {
   124→    // Add protocol if missing
   125→    if (!url.match(/^https?:\/\//)) {
   126→      url = 'https://' + url;
   127→    }
   128→
   129→    const urlObj = new URL(url);
   130→    return urlObj.hostname.replace(/^www\./, '');
   131→  } catch {
   132→    return null;
   133→  }
   134→}
   135→
   136→export default function ContractorSignupPage() {
   137→  const [step, setStep] = useState<Step>("auth");
   138→  const [isAuthenticated, setIsAuthenticated] = useState(false);
   139→  const [loading, setLoading] = useState(true);
   140→
   141→  // Auth state
   142→  const [email, setEmail] = useState("");
   143→  const [password, setPassword] = useState("");
   144→  const [authLoading, setAuthLoading] = useState(false);
   145→
   146→  // Company state
   147→  const [companyInput, setCompanyInput] = useState("");
   148→  const [companyData, setCompanyData] = useState<CompanyData | null>(null);
   149→  const [editField, setEditField] = useState<string | null>(null);
   150→  const [saving, setSaving] = useState(false);
   151→
   152→  const router = useRouter();
   153→  const supabase = createClient();
   154→
   155→  useEffect(() => {
   156→    async function checkAuth() {
   157→      const {
   158→        data: { user },
   159→      } = await supabase.auth.getUser();
   160→      if (user) {
   161→        setIsAuthenticated(true);
   162→        setStep("company");
   163→      }
   164→      setLoading(false);
   165→    }
   166→    checkAuth();
   167→  }, [supabase.auth]);
   168→
   169→  async function handleAuth(e: React.FormEvent) {
   170→    e.preventDefault();
   171→    setAuthLoading(true);
   172→
   173→    const { error } = await supabase.auth.signUp({
   174→      email,
   175→      password,
   176→      options: {
   177→        data: { type: "contractor" },
   178→        emailRedirectTo: `${window.location.origin}/auth/callback`,
   179→      },
   180→    });
   181→
   182→    if (error) {
   183→      if (error.message.includes("already registered")) {
   184→        const { error: loginError } = await supabase.auth.signInWithPassword({
   185→          email,
   186→          password,
   187→        });
   188→        if (loginError) {
   189→          toast.error(loginError.message);
   190→          setAuthLoading(false);
   191→          return;
   192→        }
   193→        // If login successful, check if email is verified
   194→        const { data: { user } } = await supabase.auth.getUser();
   195→        if (user && !user.email_confirmed_at) {
   196→          // Email not verified, redirect to verification page
   197→          router.push("/auth/verify-email");
   198→          return;
   199→        }
   200→      } else {
   201→        toast.error(error.message);
   202→        setAuthLoading(false);
   203→        return;
   204→      }
   205→    }
   206→
   207→    // Check if email verification is required
   208→    const { data: { user } } = await supabase.auth.getUser();
   209→    if (user && !user.email_confirmed_at) {
   210→      // Email not verified, redirect to verification page
   211→      router.push("/auth/verify-email");
   212→      return;
   213→    }
   214→
   215→    setIsAuthenticated(true);
   216→    setStep("company");
   217→    setAuthLoading(false);
   218→  }
   219→
   220→  async function handleCompanySearch(e: React.FormEvent) {
   221→    e.preventDefault();
   222→    if (!companyInput.trim()) return;
   223→
   224→    const input = companyInput.trim();
   225→
   226→    // Validate ABN checksum using the shared utility
   227→    if (!validateABN(input)) {
   228→      toast.error("Please enter a valid 11-digit ABN. Check the number and try again.");
   229→      return;
   230→    }
   231→
   232→    setStep("researching");
   233→
   234→    try {
   235→      const data = await lookupABN(input);
   236→      setCompanyData(data);
   237→      setStep("review");
   238→    } catch (error) {
   239→      const message = error instanceof Error ? error.message : 'Failed to lookup ABN';
   240→      toast.error(message);
   241→      setStep("company");
   242→    }
   243→  }
   244→
   245→  async function handleConfirm() {
   246→    if (!companyData) return;
   247→    setSaving(true);
   248→
   249→    try {
   250→      const {
   251→        data: { user },
   252→      } = await supabase.auth.getUser();
   253→      if (!user) {
   254→        toast.error("Session expired. Please log in again.");
   255→        router.push("/auth/login");
   256→        return;
   257→      }
   258→
   259→      const profileResult = await supabase.from("profiles").update({
   260→        name: companyData.name,
   261→        onboarding_completed: true,
   262→      }).eq("id", user.id);
   263→
   264→      if (profileResult.error) {
   265→        toast.error(`Failed to update profile: ${profileResult.error.message}`);
   266→        setSaving(false);
   267→        return;
   268→      }
   269→
   270→      const companyResult = await supabase.from("companies").insert({
   271→        profile_id: user.id,
   272→        name: companyData.name,
   273→        abn: companyData.abn,
   274→        address: companyData.address,
   275→        industry: companyData.industry,
   276→        size: companyData.size,
   277→        website: companyData.website,
   278→        ai_research: {
   279→          abr_verified: true,
   280→          abn: companyData.abn,
   281→          verified_at: new Date().toISOString(),
   282→          source: "abr",
   283→        },
   284→      });
   285→
   286→      if (companyResult.error) {
   287→        toast.error(`Failed to create company: ${companyResult.error.message}`);
   288→        setSaving(false);
   289→        return;
   290→      }
   291→
   292→      router.push("/dashboard");
   293→      router.refresh();
   294→    } catch (error) {
   295→      logger.error("Signup error:", error);
   296→      toast.error("Something went wrong. Please try again.");
   297→      setSaving(false);
   298→    }
   299→  }
   300→
   301→  function updateCompanyField(field: keyof CompanyData, value: string) {
   302→    if (!companyData) return;
   303→    setCompanyData({ ...companyData, [field]: value });
   304→    setEditField(null);
   305→  }
   306→
   307→  const totalSteps = isAuthenticated ? 3 : 4;
   308→  const currentStep =
   309→    step === "auth" ? 0 : step === "company" ? 1 : step === "researching" ? 2 : 3;
   310→
   311→  if (loading) {
   312→    return (
   313→

   314→        <Loader2 className="h-8 w-8 animate-spin text-primary" />
   315→

   316→    );
   317→  }
   318→
   319→  return (
   320→

   321→      {/* Header */}
   322→
   323→

   324→          <Link href="/" className="flex items-center gap-2">
   325→            <Image src="/logo_black.svg" alt="RateRight" width={120} height={34} className="h-7 w-auto" />
   326→          </Link>
   327→

   328→
   329→
   330→
   331→        {/* Progress bar */}
   332→

   333→          {Array.from({ length: totalSteps }, (_, i) => (
   334→            <div
   335→              key={i}
   336→              className={`h-1.5 flex-1 rounded-full transition-colors ${
   337→                i <= currentStep - (isAuthenticated ? 1 : 0)
   338→                  ? "bg-primary"
   339→                  : "bg-muted"
   340→              }`}
   341→            />
   342→          ))}
   343→

   344→
   345→        {/* Step: Auth */}
   346→        {step === "auth" && (
   347→

   348→

   349→

   350→                <Building2 className="h-5 w-5" />
   351→

   352→

   353→
Create your account

   354→

   355→                  Quick signup to get started
   356→

   357→

   358→

   359→
   360→            <form onSubmit={handleAuth} className="mt-6 space-y-4">
   361→

   362→                Email
   363→                <Input
   364→                  id="email"
   365→                  type="email"
   366→                  placeholder="you@company.com.au"
   367→                  value={email}
   368→                  onChange={(e) => setEmail(e.target.value)}
   369→                  required
   370→                  autoComplete="email"
   371→                  className="h-12 text-base"
   372→                />
   373→

   374→

   375→                Password
   376→                <Input
   377→                  id="password"
   378→                  type="password"
   379→                  placeholder="Create a password"
   380→                  value={password}
   381→                  onChange={(e) => setPassword(e.target.value)}
   382→                  required
   383→                  minLength={6}
   384→                  autoComplete="new-password"
   385→                  className="h-12 text-base"
   386→                />
   387→

   388→
   389→              <Button
   390→                type="submit"
   391→                className="h-12 w-full gap-2 text-base font-semibold"
   392→                disabled={authLoading}
   393→              >
   394→                {authLoading ? (
   395→                  <>
   396→                    <Loader2 className="h-5 w-5 animate-spin" />
   397→                    Creating account...
   398→                  </>
   399→                ) : (
   400→                  <>
   401→                    Continue
   402→                    <ArrowRight className="h-5 w-5" />
   403→                  </>
   404→                )}
   405→              </Button>
   406→            </form>
   407→
   408→

   409→              Already have an account?{" "}
   410→              <Link
   411→                href="/auth/login"
   412→                className="text-primary underline hover:no-underline"
   413→              >
   414→                Sign in
   415→              </Link>
   416→

   417→

   418→        )}
   419→
   420→        {/* Step: Enter company name/ABN */}
   421→        {step === "company" && (
   422→

   423→

   424→

   425→                <Search className="h-5 w-5" />
   426→

   427→

   428→
Your company

   429→

   430→                  We'll verify it via ABR
   431→

   432→

   433→

   434→
   435→            <form onSubmit={handleCompanySearch} className="mt-6 space-y-4">
   436→

   437→                ABN
   438→                <Input
   439→                  id="company"
   440→                  type="text"
   441→                  placeholder="Enter your 11-digit ABN"
   442→                  value={companyInput}
   443→                  onChange={(e) => setCompanyInput(e.target.value)}
   444→                  required
   445→                  autoFocus
   446→                  className="h-12 text-base"
   447→                />
   448→

   449→                  Enter your 11-digit ABN for verification via the Australian Business Register.
   450→

   451→

   452→
   453→              <Button
   454→                type="submit"
   455→                className="h-12 w-full gap-2 text-base font-semibold"
   456→              >
   457→                <Search className="h-5 w-5" />
   458→                Verify my ABN
   459→              </Button>
   460→            </form>
   461→
   462→            {!isAuthenticated && (
   463→              <button
   464→                onClick={() => setStep("auth")}
   465→                className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   466→              >
   467→                <ArrowLeft className="h-4 w-4" />
   468→                Back
   469→              </button>
   470→            )}
   471→

   472→        )}
   473→
   474→        {/* Step: ABR Verification */}
   475→        {step === "researching" && (
   476→

   477→

   478→              <Sparkles className="h-10 w-10 animate-pulse text-primary" />
   479→

   480→
Verifying your business

   481→

   482→              Checking ABR for your business details...
   483→

   484→
   485→

   486→              <ResearchStep label="Checking ABR..." />
   487→              <ResearchStep label="Verifying company..." />
   488→              <ResearchStep label="Done" />
   489→

   490→

   491→        )}
   492→
   493→        {/* Step: Review profile */}
   494→        {step === "review" && companyData && (
   495→

   496→

   497→

   498→                <CheckCircle2 className="h-5 w-5" />
   499→

   500→

   501→
Confirm your profile

   502→

   503→                  Tap any field to edit
   504→

   505→

   506→

   507→
   508→            <Card className="mt-6">
   509→              <CardContent className="space-y-4 pt-6">
   510→                <EditableField
   511→                  icon={<Building2 className="h-4 w-4" />}
   512→                  label="Company name"
   513→                  value={companyData.name}
   514→                  fieldKey="name"
   515→                  editField={editField}
   516→                  setEditField={setEditField}
   517→                  onSave={(v) => updateCompanyField("name", v)}
   518→                  logoUrl={companyData.logoUrl}
   519→                />
   520→                <EditableField
   521→                  icon={<FileText className="h-4 w-4" />}
   522→                  label="ABN"
   523→                  value={companyData.abn}
   524→                  fieldKey="abn"
   525→                  editField={editField}
   526→                  setEditField={setEditField}
   527→                  onSave={(v) => updateCompanyField("abn", v)}
   528→                />
   529→                <EditableField
   530→                  icon={<MapPin className="h-4 w-4" />}
   531→                  label="Address"
   532→                  value={companyData.address}
   533→                  fieldKey="address"
   534→                  editField={editField}
   535→                  setEditField={setEditField}
   536→                  onSave={(v) => updateCompanyField("address", v)}
   537→                />
   538→                <EditableField
   539→                  icon={<Building2 className="h-4 w-4" />}
   540→                  label="Industry"
   541→                  value={companyData.industry}
   542→                  fieldKey="industry"
   543→                  editField={editField}
   544→                  setEditField={setEditField}
   545→                  onSave={(v) => updateCompanyField("industry", v)}
   546→                />
   547→                <EditableField
   548→                  icon={<Users className="h-4 w-4" />}
   549→                  label="Company size"
   550→                  value={companyData.size}
   551→                  fieldKey="size"
   552→                  editField={editField}
   553→                  setEditField={setEditField}
   554→                  onSave={(v) => updateCompanyField("size", v)}
   555→                />
   556→                <EditableField
   557→                  icon={<Globe className="h-4 w-4" />}
   558→                  label="Website"
   559→                  value={companyData.website}
   560→                  fieldKey="website"
   561→                  editField={editField}
   562→                  setEditField={setEditField}
   563→                  onSave={(v) => updateCompanyField("website", v)}
   564→                />
   565→              </CardContent>
   566→            </Card>
   567→
   568→

   569→

   570→                <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-green-600" />
   571→

   572→

   573→                    Verified via Australian Business Register
   574→

   575→

   576→                    Your ABN has been verified. Tap any field to correct it.
   577→

   578→

   579→

   580→

   581→
   582→            <Button
   583→              onClick={handleConfirm}
   584→              className="mt-6 h-14 w-full gap-2 text-base font-semibold"
   585→              disabled={saving}
   586→            >
   587→              {saving ? (
   588→                <>
   589→                  <Loader2 className="h-5 w-5 animate-spin" />
   590→                  Saving...
   591→                </>
   592→              ) : (
   593→                <>
   594→                  <CheckCircle2 className="h-5 w-5" />
   595→                  Looks good — create my profile
   596→                </>
   597→              )}
   598→            </Button>
   599→
   600→            <button
   601→              onClick={() => setStep("company")}
   602→              className="mt-4 flex w-full items-center justify-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   603→            >
   604→              <ArrowLeft className="h-4 w-4" />
   605→              Search again
   606→            </button>
   607→

   608→        )}
   609→
   610→

   611→  );
   612→}
   613→
   614→function ResearchStep({ label }: { label: string }) {
   615→  return (
   616→

   617→      <Loader2 className="h-4 w-4 animate-spin text-primary" />
   618→      {label}
   619→

   620→  );
   621→}
   622→
   623→function EditableField({
   624→  icon,
   625→  label,
   626→  value,
   627→  fieldKey,
   628→  editField,
   629→  setEditField,
   630→  onSave,
   631→  logoUrl,
   632→}: {
   633→  icon: React.ReactNode;
   634→  label: string;
   635→  value: string;
   636→  fieldKey: string;
   637→  editField: string | null;
   638→  setEditField: (f: string | null) => void;
   639→  onSave: (value: string) => void;
   640→  logoUrl?: string | null;
   641→}) {
   642→  const [editValue, setEditValue] = useState(value);
   643→  const isEditing = editField === fieldKey;
   644→
   645→  if (isEditing) {
   646→    return (
   647→

   648→        {label}
   649→

   650→          {fieldKey === 'name' && logoUrl ? (
   651→

   652→              <img
   653→                src={logoUrl}
   654→                alt={`${value} logo`}
   655→                className="h-8 w-8 rounded-full object-cover"
   656→                onError={(e) => {
   657→                  e.currentTarget.style.display = 'none';
   658→                }}
   659→              />
   660→              <Input
   661→                value={editValue}
   662→                onChange={(e) => setEditValue(e.target.value)}
   663→                className="h-10 text-base"
   664→                autoFocus
   665→              />
   666→

   667→          ) : (
   668→            <Input
   669→              value={editValue}
   670→              onChange={(e) => setEditValue(e.target.value)}
   671→              className="h-10 text-base"
   672→              autoFocus
   673→            />
   674→          )}
   675→          <Button
   676→            size="sm"
   677→            onClick={() => onSave(editValue)}
   678→            className="h-10 shrink-0"
   679→          >
   680→            Save
   681→          </Button>
   682→

   683→

   684→    );
   685→  }
   686→
   687→  return (
   688→    <button
   689→      onClick={() => {
   690→        setEditValue(value);
   691→        setEditField(fieldKey);
   692→      }}
   693→      className="flex w-full items-start gap-3 rounded-lg p-2 text-left transition-colors hover:bg-muted"
   694→    >
   695→      {fieldKey === 'name' && logoUrl ? (
   696→

   697→          <img
   698→            src={logoUrl}
   699→            alt={`${value} logo`}
   700→            className="h-6 w-6 rounded-full object-cover"
   701→            onError={(e) => {
   702→              // If image fails to load, fall back to icon
   703→              e.currentTarget.style.display = 'none';
   704→            }}
   705→          />
   706→

   707→      ) : (
   708→
{icon}

   709→      )}
   710→

   711→
{label}

   712→
{value}

   713→

   714→      <Edit3 className="mt-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
   715→    </button>
   716→  );
   717→}
   718→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→import { redirect } from "next/navigation";
     2→import Link from "next/link";
     3→import { createClient } from "@/lib/supabase/server";
     4→import { Button } from "@/components/ui/button";
     5→import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
     6→import { Badge } from "@/components/ui/badge";
     7→import {
     8→  Building2,
     9→  MapPin,
    10→  Globe,
    11→  Users,
    12→  Edit3,
    13→  Star,
    14→  ArrowLeft,
    15→} from "lucide-react";
    16→import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
    17→import { AvatarUpload } from "@/components/avatar-upload";
    18→import { RatingsSummary } from "@/components/ratings-summary";
    19→import { RatingsList } from "@/components/ratings-list";
    20→
    21→export default async function CompanyProfilePage() {
    22→  const supabase = await createClient();
    23→
    24→  const {
    25→    data: { user },
    26→  } = await supabase.auth.getUser();
    27→
    28→  if (!user) {
    29→    redirect("/auth/login");
    30→  }
    31→
    32→  // Fetch company profile
    33→  const { data: company } = await supabase
    34→    .from("companies")
    35→    .select("*")
    36→    .eq("profile_id", user.id)
    37→    .single();
    38→
    39→  if (!company) {
    40→    redirect("/contractor/signup");
    41→  }
    42→
    43→  // Fetch ratings with profile information
    44→  const { data: ratings } = await supabase
    45→    .from("ratings")
    46→    .select(`
    47→      *,
    48→      from_profile:profiles!ratings_from_id_fkey(
    49→        id,
    50→        name,
    51→        avatar_url
    52→      )
    53→    `)
    54→    .eq("to_id", user.id)
    55→    .order("created_at", { ascending: false })
    56→    .limit(3); // Show latest 3 ratings
    57→
    58→  const avgRating = ratings && ratings.length > 0
    59→    ? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
    60→    : 0;
    61→
    62→  const totalRatings = ratings?.length || 0;
    63→
    64→  return (
    65→

    66→
    67→

    68→          <Button asChild variant="ghost" size="icon">
    69→            <Link href="/dashboard">
    70→              <ArrowLeft className="h-5 w-5" />
    71→            </Link>
    72→          </Button>
    73→
Company Profile

    74→

    75→
    76→        {/* Company header */}
    77→

    78→          <AvatarUpload
    79→            currentUrl={company.logo_url}
    80→            userName={company.name}
    81→          />
    82→
    83→
{company.name}

    84→
    85→

    86→            {company.address && (
    87→
    88→                <MapPin className="h-3.5 w-3.5" />
    89→                {company.address}
    90→
    91→            )}
    92→            {company.industry && (
    93→
    94→                <Building2 className="h-3.5 w-3.5" />
    95→                {company.industry}
    96→
    97→            )}
    98→            {avgRating > 0 && (
    99→
   100→                ⭐ {avgRating.toFixed(1)} ({totalRatings})
   101→
   102→            )}
   103→

   104→
   105→          {company.size && (
   106→            <Badge variant="outline" className="mt-2">
   107→              <Users className="mr-1 h-3 w-3" />
   108→              {company.size}
   109→            </Badge>
   110→          )}
   111→

   112→
   113→        {/* Company info */}
   114→

   115→          {company.abn && (
   116→            <Card>
   117→              <CardContent className="pt-5">
   118→

   119→                  <Building2 className="h-4 w-4" />
   120→                  ABN
   121→

   122→
{company.abn}

   123→              </CardContent>
   124→            </Card>
   125→          )}
   126→
   127→          {company.website && (
   128→            <Card>
   129→              <CardContent className="pt-5">
   130→

   131→                  <Globe className="h-4 w-4" />
   132→                  Website
   133→

   134→
   140→                  {company.website}
   141→
   142→              </CardContent>
   143→            </Card>
   144→          )}
   145→
   146→          {company.ai_research && (
   147→            <Card>
   148→              <CardContent className="pt-5">
   149→

   150→                  <Building2 className="h-4 w-4" />
   151→                  About
   152→

   153→

   154→                  {(company.ai_research as { description?: string })?.description ||
   155→                   (company.ai_research as { summary?: string })?.summary ||
   156→                   "Company information available."}
   157→

   158→              </CardContent>
   159→            </Card>
   160→          )}
   161→

   162→
   163→        {/* Ratings */}
   164→        {(ratings && ratings.length > 0) && (
   165→

   166→            <RatingsSummary
   167→              averageRating={avgRating}
   168→              totalRatings={totalRatings}
   169→            />
   170→
   171→

   172→              <RatingsList
   173→                ratings={ratings}
   174→                title="Recent Reviews"
   175→              />
   176→
   177→              {totalRatings > 3 && (
   178→                <Button asChild variant="outline" className="w-full mt-4">
   179→                  <Link href={`/ratings/${user.id}`}>
   180→                    <Star className="h-4 w-4 mr-2" />
   181→                    View All Reviews ({totalRatings})
   182→                  </Link>
   183→                </Button>
   184→              )}
   185→

   186→

   187→        )}
   188→
   189→        {/* Edit button */}
   190→

   191→          <Button asChild variant="outline" className="w-full h-12 text-base gap-2">
   192→            <Link href="/contractor/signup">
   193→              <Edit3 className="h-5 w-5" />
   194→              Edit Profile
   195→            </Link>
   196→          </Button>
   197→

   198→
   199→
   200→      <BottomNavWrapper userType="contractor" />
   201→

   202→  );
   203→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→"use client";
     2→
     3→import { useState } from "react";
     4→import { useRouter } from "next/navigation";
     5→import { logger } from "@/lib/logger";
     6→import Link from "next/link";
     7→import toast from "react-hot-toast";
     8→import { createClient } from "@/lib/supabase/client";
     9→import { Button } from "@/components/ui/button";
    10→import { Card, CardContent } from "@/components/ui/card";
    11→import { Input } from "@/components/ui/input";
    12→import { Label } from "@/components/ui/label";
    13→import { Badge } from "@/components/ui/badge";
    14→import {
    15→  ArrowLeft,
    16→  ArrowRight,
    17→  Loader2,
    18→  Sparkles,
    19→  Check,
    20→  MapPin,
    21→  Calendar,
    22→  Users,
    23→  DollarSign,
    24→  HardHat,
    25→  Wrench,
    26→  Hammer,
    27→  Blocks,
    28→  Fence,
    29→  CircleDot,
    30→  Shovel,
    31→} from "lucide-react";
    32→import { jsonToPostgresPoint } from "@/lib/location-utils";
    33→import { csrfFetch } from "@/lib/csrf-client";
    34→
    35→const TRADE_CATEGORIES = [
    36→  { id: "steel-fixer", label: "Steel Fixer", icon: Wrench, color: "bg-blue-500/10 border-blue-500/30 text-blue-700" },
    37→  { id: "formworker", label: "Formworker", icon: Blocks, color: "bg-amber-500/10 border-amber-500/30 text-amber-700" },
    38→  { id: "labourer", label: "Labourer", icon: Shovel, color: "bg-green-500/10 border-green-500/30 text-green-700" },
    39→  { id: "concreter", label: "Concreter", icon: CircleDot, color: "bg-gray-500/10 border-gray-500/30 text-gray-700" },
    40→  { id: "carpenter", label: "Carpenter", icon: Hammer, color: "bg-orange-500/10 border-orange-500/30 text-orange-700" },
    41→  { id: "scaffolder", label: "Scaffolder", icon: Fence, color: "bg-purple-500/10 border-purple-500/30 text-purple-700" },
    42→  { id: "other", label: "Other", icon: HardHat, color: "bg-muted border-border text-foreground" },
    43→];
    44→
    45→const AI_SUGGESTIONS: Record<string, { title: string; description: string; certs: string[]; rateMin: number; rateMax: number; rateType: "hourly" | "daily" }> = {
    46→  "steel-fixer": {
    47→    title: "Steel Fixer Needed",
    48→    description: "Experienced steel fixer required for commercial construction project. Must be able to read drawings, tie rebar, and work with mesh. PPE required on site at all times. Early starts — 6:30am.",
    49→    certs: ["White Card", "Working at Heights"],
    50→    rateMin: 45,
    51→    rateMax: 65,
    52→    rateType: "hourly",
    53→  },
    54→  "formworker": {
    55→    title: "Formworker Required",
    56→    description: "Skilled formworker needed for concrete pours on a residential build. Experience with timber and steel formwork systems. Must be comfortable with early starts and physical work.",
    57→    certs: ["White Card"],
    58→    rateMin: 42,
    59→    rateMax: 58,
    60→    rateType: "hourly",
    61→  },
    62→  "labourer": {
    63→    title: "General Labourer Needed",
    64→    description: "Reliable labourer needed for site clean-up, material handling, and general duties. No specific trade experience needed but must be fit and punctual. PPE and boots required.",
    65→    certs: ["White Card"],
    66→    rateMin: 32,
    67→    rateMax: 42,
    68→    rateType: "hourly",
    69→  },
    70→  "concreter": {
    71→    title: "Concreter Required",
    72→    description: "Experienced concreter needed for slab pours and finishing work. Must know how to screed, float, and finish concrete to a high standard. Early morning pours expected.",
    73→    certs: ["White Card"],
    74→    rateMin: 45,
    75→    rateMax: 65,
    76→    rateType: "hourly",
    77→  },
    78→  "carpenter": {
    79→    title: "Carpenter Needed",
    80→    description: "Qualified carpenter required for framing and fit-out work. Must have own basic tools. Experience with residential or commercial builds preferred.",
    81→    certs: ["White Card", "Trade Certificate"],
    82→    rateMin: 45,
    83→    rateMax: 65,
    84→    rateType: "hourly",
    85→  },
    86→  "scaffolder": {
    87→    title: "Scaffolder Required",
    88→    description: "Licensed scaffolder needed for erection and dismantling of scaffolding systems. Must hold current scaffolding licence. Safety-first attitude essential.",
    89→    certs: ["White Card", "Scaffolding Licence", "Working at Heights"],
    90→    rateMin: 48,
    91→    rateMax: 68,
    92→    rateType: "hourly",
    93→  },
    94→  "other": {
    95→    title: "Construction Worker Needed",
    96→    description: "Worker needed for construction project. Please specify the role and requirements in the description.",
    97→    certs: ["White Card"],
    98→    rateMin: 35,
    99→    rateMax: 55,
   100→    rateType: "hourly",
   101→  },
   102→};
   103→
   104→export default function PostJobPage() {
   105→  const router = useRouter();
   106→  const supabase = createClient();
   107→
   108→  const [step, setStep] = useState(1);
   109→  const [loading, setLoading] = useState(false);
   110→  const [aiLoading, setAiLoading] = useState(false);
   111→
   112→  // Step 1: Trade
   113→  const [selectedTrade, setSelectedTrade] = useState("");
   114→
   115→  // Step 2: AI-filled job details + location/date/workers (COMBINED)
   116→  const [title, setTitle] = useState("");
   117→  const [description, setDescription] = useState("");
   118→  const [certs, setCerts] = useState<string[]>([]);
   119→  const [rateMin, setRateMin] = useState("");
   120→  const [rateMax, setRateMax] = useState("");
   121→  const [rateType, setRateType] = useState<"hourly" | "daily">("hourly");
   122→  const [suburb, setSuburb] = useState("");
   123→  const [siteAddress, setSiteAddress] = useState("");
   124→  const [startDate, setStartDate] = useState("");
   125→  const [duration, setDuration] = useState("");
   126→  const [workersNeeded, setWorkersNeeded] = useState("1");
   127→
   128→  async function handleTradeSelect(tradeId: string) {
   129→    setSelectedTrade(tradeId);
   130→    setAiLoading(true);
   131→    setStep(2);
   132→
   133→    // Try real AI generation, fall back to hardcoded suggestions
   134→    try {
   135→      const res = await csrfFetch("/api/ai/generate-job", {
   136→        method: "POST",
   137→        headers: { "Content-Type": "application/json" },
   138→        body: JSON.stringify({ trade: tradeId }),
   139→      });
   140→
   141→      if (res.ok) {
   142→        const data = await res.json();
   143→        setTitle(data.title);
   144→        setDescription(data.description);
   145→        setCerts(data.certifications);
   146→        setRateMin(String(data.rateMin));
   147→        setRateMax(String(data.rateMax));
   148→        setRateType(data.rateType);
   149→        setAiLoading(false);
   150→        return;
   151→      }
   152→    } catch {
   153→      // Fall through to hardcoded fallback
   154→    }
   155→
   156→    // Fallback: use hardcoded suggestions
   157→    const suggestion = AI_SUGGESTIONS[tradeId];
   158→    if (suggestion) {
   159→      setTitle(suggestion.title);
   160→      setDescription(suggestion.description);
   161→      setCerts(suggestion.certs);
   162→      setRateMin(String(suggestion.rateMin));
   163→      setRateMax(String(suggestion.rateMax));
   164→      setRateType(suggestion.rateType);
   165→    }
   166→    setAiLoading(false);
   167→  }
   168→
   169→  async function handlePost() {
   170→    setLoading(true);
   171→
   172→    try {
   173→      const { data: { user } } = await supabase.auth.getUser();
   174→      if (!user) {
   175→        toast.error("Session expired. Please log in again.");
   176→        router.push("/auth/login");
   177→        return;
   178→      }
   179→
   180→      // Get contractor's company
   181→      const { data: company } = await supabase
   182→        .from("companies")
   183→        .select("id")
   184→        .eq("profile_id", user.id)
   185→        .single();
   186→
   187→      if (!company) {
   188→        toast.error("Please complete your company profile first.");
   189→        setLoading(false);
   190→        return;
   191→      }
   192→
   193→      // Geocode the suburb to get lat/lng coordinates
   194→      let location: string | null = null;
   195→      if (suburb) {
   196→        try {
   197→          const geocodeRes = await csrfFetch("/api/geocode", {
   198→            method: "POST",
   199→            headers: { "Content-Type": "application/json" },
   200→            body: JSON.stringify({ suburb }),
   201→          });
   202→
   203→          if (geocodeRes.ok) {
   204→            const geocodeData = await geocodeRes.json();
   205→            if (geocodeData.location) {
   206→              location = jsonToPostgresPoint(geocodeData.location);
   207→            }
   208→          }
   209→        } catch (geoError) {
   210→          // Continue without location - suburb text will still be saved
   211→        }
   212→      }
   213→
   214→      const { error: insertError } = await supabase.from("jobs").insert({
   215→        company_id: company.id,
   216→        title,
   217→        description,
   218→        trade: selectedTrade,
   219→        suburb,
   220→        location,
   221→        site_address: siteAddress,
   222→        rate_min: parseFloat(rateMin) || null,
   223→        rate_max: parseFloat(rateMax) || null,
   224→        rate_type: rateType,
   225→        start_date: startDate || null,
   226→        duration: duration || null,
   227→        workers_needed: parseInt(workersNeeded) || 1,
   228→        certifications_required: certs,
   229→        status: "active",
   230→        ai_generated: true,
   231→      });
   232→
   233→      if (insertError) {
   234→        toast.error(`Failed to post job: ${insertError.message}`);
   235→        setLoading(false);
   236→        return;
   237→      }
   238→
   239→      setStep(3); // Success (was step 5, now step 3)
   240→    } catch (error) {
   241→      logger.error("Job post error:", error);
   242→      toast.error("Something went wrong. Please try again.");
   243→      setLoading(false);
   244→    }
   245→  }
   246→
   247→  return (
   248→

   249→
   250→        {/* STEP 1: Select Trade */}
   251→        {step === 1 && (
   252→

   253→
What trade do you need?

   254→

   255→              Tap to select. AI will write the job post for you.
   256→

   257→

   258→              {TRADE_CATEGORIES.map((trade) => {
   259→                const Icon = trade.icon;
   260→                return (
   261→                  <button
   262→                    key={trade.id}
   263→                    onClick={() => handleTradeSelect(trade.id)}
   264→                    className={`flex flex-col items-center justify-center gap-3 rounded-2xl border-2 p-6 min-h-[120px] text-center transition-all active:scale-95 ${trade.color}`}
   265→                  >
   266→                    <Icon className="h-10 w-10" />
   267→                    {trade.label}
   268→                  </button>
   269→                );
   270→              })}
   271→

   272→

   273→        )}
   274→
   275→        {/* STEP 2: AI-filled Job Details + Location/Date/Workers (COMBINED) */}
   276→        {step === 2 && (
   277→

   278→            {aiLoading ? (
   279→

   280→

   281→                  <Sparkles className="h-12 w-12 text-primary animate-pulse" />
   282→

   283→
AI writing your job post...

   284→

   285→                  Based on market rates and common requirements
   286→

   287→

   288→            ) : (
   289→

   290→

   291→                  <Sparkles className="h-5 w-5 text-primary" />
   292→
Job Details

   293→

   294→

   295→                  AI pre-filled these. Edit anything you like.
   296→

   297→
   298→

   299→                  {/* Job Details Section */}
   300→

   301→                    Job Title
   302→                    <Input
   303→                      id="title"
   304→                      value={title}
   305→                      onChange={(e) => setTitle(e.target.value)}
   306→                      className="mt-2 h-14 text-base"
   307→                    />
   308→

   309→
   310→

   311→                    Description
   312→                    <textarea
   313→                      id="description"
   314→                      value={description}
   315→                      onChange={(e) => setDescription(e.target.value)}
   316→                      rows={5}
   317→                      className="mt-2 flex w-full rounded-md border border-input bg-background px-4 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
   318→                    />
   319→

   320→
   321→

   322→                    Pay Rate (AUD)
   323→

   324→

   325→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   326→                        <Input
   327→                          type="number"
   328→                          value={rateMin}
   329→                          onChange={(e) => setRateMin(e.target.value)}
   330→                          placeholder="Min"
   331→                          className="h-14 pl-10 text-base"
   332→                        />
   333→

   334→                      to
   335→

   336→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   337→                        <Input
   338→                          type="number"
   339→                          value={rateMax}
   340→                          onChange={(e) => setRateMax(e.target.value)}
   341→                          placeholder="Max"
   342→                          className="h-14 pl-10 text-base"
   343→                        />
   344→

   345→

   346→

   347→                      <Button
   348→                        type="button"
   349→                        variant={rateType === "hourly" ? "default" : "outline"}
   350→                        onClick={() => setRateType("hourly")}
   351→                        className="h-12 text-base font-semibold"
   352→                      >
   353→                        Per Hour
   354→                      </Button>
   355→                      <Button
   356→                        type="button"
   357→                        variant={rateType === "daily" ? "default" : "outline"}
   358→                        onClick={() => setRateType("daily")}
   359→                        className="h-12 text-base font-semibold"
   360→                      >
   361→                        Per Day
   362→                      </Button>
   363→

   364→

   365→
   366→                  {/* Location Section */}
   367→

   368→

   369→                      <MapPin className="h-5 w-5" />
   370→                      Location
   371→

   372→
   373→

   374→

   375→                        Site Address
   376→                        <Input
   377→                          id="siteAddress"
   378→                          value={siteAddress}
   379→                          onChange={(e) => setSiteAddress(e.target.value)}
   380→                          placeholder="e.g. 123 George St, Sydney"
   381→                          className="mt-2 h-14 text-base"
   382→                        />
   383→

   384→
   385→

   386→                        Suburb
   387→                        <Input
   388→                          id="suburb"
   389→                          value={suburb}
   390→                          onChange={(e) => setSuburb(e.target.value)}
   391→                          placeholder="e.g. Parramatta"
   392→                          className="mt-2 h-14 text-base"
   393→                        />
   394→

   395→

   396→

   397→
   398→                  {/* Date & Workers Section */}
   399→

   400→

   401→                      <Calendar className="h-5 w-5" />
   402→                      Schedule
   403→

   404→
   405→

   406→

   407→                        Start Date
   408→                        <Input
   409→                          id="startDate"
   410→                          type="date"
   411→                          value={startDate}
   412→                          onChange={(e) => setStartDate(e.target.value)}
   413→                          className="mt-2 h-14 text-base"
   414→                        />
   415→

   416→
   417→

   418→                        Duration
   419→                        <Input
   420→                          id="duration"
   421→                          value={duration}
   422→                          onChange={(e) => setDuration(e.target.value)}
   423→                          placeholder="e.g. 2 weeks, ongoing"
   424→                          className="mt-2 h-14 text-base"
   425→                        />
   426→

   427→
   428→

   429→
   430→                          <Users className="h-5 w-5" />
   431→                          Workers Needed
   432→
   433→

   434→                          <Button
   435→                            type="button"
   436→                            variant="outline"
   437→                            onClick={() =>
   438→                              setWorkersNeeded(String(Math.max(1, parseInt(workersNeeded) - 1)))
   439→                            }
   440→                            className="h-14 w-14 text-2xl font-bold"
   441→                          >
   442→                            -
   443→                          </Button>
   444→
   445→                            {workersNeeded}
   446→
   447→                          <Button
   448→                            type="button"
   449→                            variant="outline"
   450→                            onClick={() =>
   451→                              setWorkersNeeded(String(parseInt(workersNeeded) + 1))
   452→                            }
   453→                            className="h-14 w-14 text-2xl font-bold"
   454→                          >
   455→                            +
   456→                          </Button>
   457→

   458→

   459→

   460→

   461→
   462→                  {/* Required Certifications */}
   463→                  {certs.length > 0 && (
   464→

   465→                      Required Certifications
   466→

   467→                        {certs.map((cert) => (
   468→                          <Badge key={cert} variant="secondary" className="text-sm py-1.5 px-3">
   469→                            {cert}
   470→                            <button
   471→                              onClick={() => setCerts(certs.filter((c) => c !== cert))}
   472→                              className="ml-2 text-muted-foreground hover:text-foreground"
   473→                            >
   474→                              ×
   475→                            </button>
   476→                          </Badge>
   477→                        ))}
   478→

   479→

   480→                  )}
   481→

   482→

   483→            )}
   484→

   485→        )}
   486→
   487→        {/* STEP 3: Success */}
   488→        {step === 3 && (
   489→          <Card className="mt-8">
   490→            <CardContent className="flex flex-col items-center py-10 text-center">
   491→

   492→                <Check className="h-10 w-10 text-green-600" />
   493→

   494→
Job Posted!

   495→

   496→                We're matching you with the best workers now.
   497→                You'll get notified when we find matches.
   498→

   499→

   500→                <Button
   501→                  onClick={() => router.push("/contractor/matches")}
   502→                  className="w-full h-12 text-base font-semibold"
   503→                >
   504→                  View Matches
   505→                </Button>
   506→                <Button
   507→                  onClick={() => router.push("/dashboard")}
   508→                  variant="outline"
   509→                  className="w-full h-12 text-base"
   510→                >
   511→                  Back to Dashboard
   512→                </Button>
   513→

   514→            </CardContent>
   515→          </Card>
   516→        )}
   517→
   518→
   519→      {/* Sticky Bottom CTA (Step 2 only) */}
   520→      {step === 2 && !aiLoading && (
   521→

   522→

   523→            <Button
   524→              onClick={handlePost}
   525→              className="w-full h-12 text-base font-semibold"
   526→              disabled={loading || !title || !suburb}
   527→            >
   528→              {loading ? (
   529→                <>
   530→                  <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   531→                  Posting...
   532→                </>
   533→              ) : (
   534→                <>
   535→                  Post Job Now
   536→                  <Check className="ml-2 h-6 w-6" />
   537→                </>
   538→              )}
   539→            </Button>
   540→

   541→              Free to post. You only pay $50 when you hire someone.
   542→

   543→

   544→

   545→      )}
   546→

   547→  );
   548→}
   549→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now let me continue reading the other critical pages:


1→"use client";
     2→
     3→import { useEffect, useState, useRef } from "react";
     4→import Link from "next/link";
     5→import toast from "react-hot-toast";
     6→import { logger } from "@/lib/logger";
     7→import { createClient } from "@/lib/supabase/client";
     8→import { Card, CardContent } from "@/components/ui/card";
     9→import { Badge } from "@/components/ui/badge";
    10→import { Button } from "@/components/ui/button";
    11→import { BottomNav } from "@/components/bottom-nav";
    12→import { HirePaymentModal } from "@/components/payment/HirePaymentModal";
    13→import {
    14→  ArrowLeft,
    15→  Loader2,
    16→  Star,
    17→  MapPin,
    18→  Clock,
    19→  CheckCircle2,
    20→  UserCheck,
    21→  HardHat,
    22→  AlertCircle,
    23→} from "lucide-react";
    24→import type { Job } from "@/lib/types";
    25→import { postgresPointToJson } from "@/lib/location-utils";
    26→import { TRADE_CATEGORIES } from "@/lib/constants";
    27→import { PAYMENT_AMOUNT } from "@/lib/constants";
    28→
    29→interface MatchWithWorker {
    30→  id: string;
    31→  job_id: string;
    32→  worker_id: string;
    33→  status: string;
    34→  match_score: number | null;
    35→  ai_summary: string | null;
    36→  contractor_action: string;
    37→  worker_action: string;
    38→  hired_at: string | null;
    39→  fee_charged: boolean;
    40→  created_at: string;
    41→  worker: {
    42→    id: string;
    43→    name: string | null;
    44→    suburb: string | null;
    45→    avatar_url: string | null;
    46→  } | null;
    47→  worker_profile: {
    48→    trades: string[];
    49→    experience_years: number | null;
    50→    certifications: string[];
    51→    availability: string | null;
    52→  } | null;
    53→}
    54→
    55→interface SwipeableMatchCardProps {
    56→  match: MatchWithWorker;
    57→  isHired: boolean;
    58→  justHired: boolean;
    59→  hiring: boolean;
    60→  onHire: (match: MatchWithWorker) => void;
    61→  onReject: (match: MatchWithWorker) => void;
    62→}
    63→
    64→function SwipeableMatchCard({ match, isHired, justHired, hiring, onHire, onReject }: SwipeableMatchCardProps) {
    65→  const [swipeX, setSwipeX] = useState(0);
    66→  const [swiping, setSwiping] = useState(false);
    67→  const startX = useRef(0);
    68→  const currentX = useRef(0);
    69→
    70→  const summary = match.ai_summary || generateAiSummary(match.worker, match.worker_profile);
    71→
    72→  const handleTouchStart = (e: React.TouchEvent) => {
    73→    if (isHired) return;
    74→    startX.current = e.touches[0].clientX;
    75→    setSwiping(true);
    76→  };
    77→
    78→  const handleTouchMove = (e: React.TouchEvent) => {
    79→    if (!swiping || isHired) return;
    80→    currentX.current = e.touches[0].clientX;
    81→    const diff = currentX.current - startX.current;
    82→    // Allow both directions
    83→    setSwipeX(Math.max(-150, Math.min(diff, 150)));
    84→  };
    85→
    86→  const handleTouchEnd = () => {
    87→    if (!swiping || isHired) return;
    88→    setSwiping(false);
    89→
    90→    // If swiped right > 100px, hire
    91→    if (swipeX > 100) {
    92→      onHire(match);
    93→    }
    94→    // If swiped left > 100px, reject
    95→    else if (swipeX < -100) {
    96→      onReject(match);
    97→    }
    98→
    99→    // Reset
   100→    setSwipeX(0);
   101→  };
   102→
   103→  const swipeProgress = Math.abs(swipeX) / 100;
   104→  const swipeDirection = swipeX > 0 ? "right" : "left";
   105→
   106→  return (
   107→

   108→      {/* Swipe indicator backgrounds */}
   109→      {!isHired && swipeX !== 0 && (
   110→        <>
   111→          {/* Right swipe (hire) */}
   112→          {swipeX > 0 && (
   113→

   120→              <UserCheck className="h-8 w-8 text-green-600" />
   121→
   122→                {swipeProgress >= 1 ? "Release to hire" : "Swipe to hire"}
   123→
   124→

   125→          )}
   126→          {/* Left swipe (reject) */}
   127→          {swipeX < 0 && (
   128→

   135→
   136→                {swipeProgress >= 1 ? "Release to pass" : "Swipe to pass"}
   137→
   138→              <AlertCircle className="h-8 w-8 text-red-600" />
   139→

   140→          )}
   141→        </>
   142→      )}
   143→
   144→      <Card
   145→        className={`transition-all touch-pan-y ${justHired ? "ring-2 ring-green-500" : ""}`}
   146→        style={{
   147→          transform: `translateX(${swipeX}px)`,
   148→          transition: swiping ? 'none' : 'transform 0.2s ease-out',
   149→        }}
   150→        onTouchStart={handleTouchStart}
   151→        onTouchMove={handleTouchMove}
   152→        onTouchEnd={handleTouchEnd}
   153→      >
   154→        <CardContent className="p-5">
   155→

   156→            {/* Avatar */}
   157→

   158→              {match.worker?.name?.[0]?.toUpperCase() || "?"}
   159→

   160→
   161→

   162→

   163→

   164→                  {match.worker?.name || "Worker"}
   165→

   166→                {isHired && (
   167→                  <Badge className="bg-green-600 text-xs shrink-0">Hired</Badge>
   168→                )}
   169→

   170→
   171→              {/* AI Summary */}
   172→

   173→                {summary}
   174→

   175→
   176→              {/* Details */}
   177→

   178→                {match.worker_profile?.experience_years && (
   179→
   180→                    <Clock className="h-4 w-4" />
   181→                    {match.worker_profile.experience_years}yr exp
   182→
   183→                )}
   184→                {match.worker?.suburb && (
   185→
   186→                    <MapPin className="h-4 w-4" />
   187→                    {match.worker.suburb}
   188→
   189→                )}
   190→                {match.match_score && (
   191→
   192→                    <Star className="h-4 w-4 fill-amber-400 text-amber-400" />
   193→                    {Math.round(match.match_score)}% match
   194→
   195→                )}
   196→

   197→
   198→              {/* Certs */}
   199→              {match.worker_profile?.certifications && match.worker_profile.certifications.length > 0 && (
   200→

   201→                  {match.worker_profile.certifications.slice(0, 3).map((cert) => (
   202→                    <Badge key={cert} variant="outline" className="text-xs px-2 py-1">
   203→                      {cert}
   204→                    </Badge>
   205→                  ))}
   206→

   207→              )}
   208→

   209→

   210→
   211→          {/* Action */}
   212→          {!isHired && (
   213→            <Button
   214→              onClick={() => onHire(match)}
   215→              className="mt-4 w-full h-12 text-base font-semibold"
   216→              disabled={hiring}
   217→            >
   218→              {hiring ? (
   219→                <>
   220→                  <Loader2 className="mr-2 h-5 w-5 animate-spin" />
   221→                  Processing...
   222→                </>
   223→              ) : (
   224→                <>
   225→                  <UserCheck className="mr-2 h-5 w-5" />
   226→                  Hire — $50 Fee
   227→                </>
   228→              )}
   229→            </Button>
   230→          )}
   231→
   232→          {justHired && (
   233→

   234→              <CheckCircle2 className="h-5 w-5" />
   235→              Hired! Worker has been notified.
   236→

   237→          )}
   238→
   239→          {isHired && !justHired && (
   240→

   241→              <CheckCircle2 className="h-5 w-5 text-green-600" />
   242→              Hired on{" "}
   243→              {match.hired_at
   244→                ? new Date(match.hired_at).toLocaleDateString("en-AU", {
   245→                    day: "numeric",
   246→                    month: "short",
   247→                  })
   248→                : "recently"}
   249→

   250→          )}
   251→        </CardContent>
   252→      </Card>
   253→

   254→  );
   255→}
   256→
   257→const TRADE_LABEL: Record<string, string> = TRADE_CATEGORIES.reduce((acc, cat) => {
   258→  acc[cat.id] = cat.label;
   259→  return acc;
   260→}, {} as Record<string, string>);
   261→
   262→function generateAiSummary(
   263→  worker: MatchWithWorker["worker"],
   264→  profile: MatchWithWorker["worker_profile"]
   265→): string {
   266→  const parts: string[] = [];
   267→  if (profile?.trades?.length) {
   268→    parts.push(profile.trades.map((t) => TRADE_LABEL[t] || t).join(", "));
   269→  }
   270→  if (profile?.experience_years) {
   271→    parts.push(`${profile.experience_years} years exp`);
   272→  }
   273→  if (worker?.suburb) {
   274→    parts.push(worker.suburb);
   275→  }
   276→  if (profile?.availability) {
   277→    parts.push(profile.availability);
   278→  }
   279→  if (profile?.certifications?.length) {
   280→    parts.push(profile.certifications.slice(0, 2).join(", "));
   281→  }
   282→  return parts.join(" \u00B7 ") || "Worker profile";
   283→}
   284→
   285→export default function ContractorMatchesPage() {
   286→  const supabase = createClient();
   287→  const [jobs, setJobs] = useState<Job[]>([]);
   288→  const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
   289→  const [matches, setMatches] = useState<MatchWithWorker[]>([]);
   290→  const [loading, setLoading] = useState(true);
   291→  const [loadingMatches, setLoadingMatches] = useState(false);
   292→  const [hiring, setHiring] = useState<string | null>(null);
   293→  const [hireSuccess, setHireSuccess] = useState<string | null>(null);
   294→  const [currentPage, setCurrentPage] = useState(1);
   295→  const [totalPages, setTotalPages] = useState(1);
   296→  const [totalCount, setTotalCount] = useState(0);
   297→  const ITEMS_PER_PAGE = 10;
   298→
   299→  // Payment modal state
   300→  const [companyId, setCompanyId] = useState<string | null>(null);
   301→  const [selectedMatch, setSelectedMatch] = useState<MatchWithWorker | null>(null);
   302→  const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
   303→
   304→  // Load contractor's jobs
   305→  useEffect(() => {
   306→    async function load() {
   307→      const { data: { user } } = await supabase.auth.getUser();
   308→      if (!user) return;
   309→
   310→      const { data: company } = await supabase
   311→        .from("companies")
   312→        .select("id")
   313→        .eq("profile_id", user.id)
   314→        .single();
   315→
   316→      if (!company) {
   317→        setLoading(false);
   318→        return;
   319→      }
   320→
   321→      setCompanyId(company.id);
   322→
   323→      const { data: jobsData } = await supabase
   324→        .from("jobs")
   325→        .select("id, company_id, status, created_at")
   326→        .eq("company_id", company.id)
   327→        .in("status", ["active", "filled"])
   328→        .order("created_at", { ascending: false });
   329→
   330→      if (jobsData && jobsData.length > 0) {
   331→        // Convert PostgreSQL point to JSON format
   332→        const convertedJobs = jobsData.map((job: any) => ({
   333→          ...job,
   334→          location: postgresPointToJson(job.location)
   335→        }));
   336→        setJobs(convertedJobs);
   337→        setSelectedJobId(jobsData[0].id);
   338→      }
   339→      setLoading(false);
   340→    }
   341→    load();
   342→  }, [supabase]);
   343→
   344→  // Load matches for selected job
   345→  useEffect(() => {
   346→    if (!selectedJobId) return;
   347→    async function loadMatches() {
   348→      setLoadingMatches(true);
   349→
   350→      // Get total count first
   351→      const { count } = await supabase
   352→        .from("matches")
   353→        .select("*", { count: "exact", head: true })
   354→        .eq("job_id", selectedJobId);
   355→
   356→      if (count) {
   357→        setTotalCount(count);
   358→        setTotalPages(Math.ceil(count / ITEMS_PER_PAGE));
   359→      }
   360→
   361→      // Calculate pagination range
   362→      const from = (currentPage - 1) * ITEMS_PER_PAGE;
   363→      const to = from + ITEMS_PER_PAGE - 1;
   364→
   365→      // Get matches for this job with pagination
   366→      const { data: matchData } = await supabase
   367→        .from("matches")
   368→        .select("id, job_id, worker_id, status, match_score, ai_summary, contractor_action, worker_action, hired_at, fee_charged, created_at")
   369→        .eq("job_id", selectedJobId)
   370→        .order("match_score", { ascending: false })
   371→        .range(from, to);
   372→
   373→      if (!matchData || matchData.length === 0) {
   374→        setMatches([]);
   375→        setLoadingMatches(false);
   376→        return;
   377→      }
   378→
   379→      // Fetch worker profiles and base profiles
   380→      const workerIds: string[] = [];
   381→      for (const m of matchData) {
   382→        if (m.worker_id) workerIds.push(m.worker_id);
   383→      }
   384→
   385→      if (workerIds.length === 0) {
   386→        setMatches([]);
   387→        setLoadingMatches(false);
   388→        return;
   389→      }
   390→
   391→      const [{ data: profiles }, { data: workerProfiles }] = await Promise.all([
   392→        supabase.from("profiles").select("id, name, suburb, avatar_url").in("id", workerIds),
   393→        supabase.from("worker_profiles").select("profile_id, trades, experience_years, certifications, availability").in("profile_id", workerIds),
   394→      ]);
   395→
   396→      type ProfileRow = { id: string; name: string | null; suburb: string | null; avatar_url: string | null };
   397→      type WPRow = { profile_id: string; trades: string[]; experience_years: number | null; certifications: string[]; availability: string | null };
   398→
   399→      const profileMap = new Map((profiles || []).map((p: ProfileRow) => [p.id, p]));
   400→      const wpMap = new Map((workerProfiles || []).map((w: WPRow) => [w.profile_id, w]));
   401→
   402→      const enriched: MatchWithWorker[] = matchData.map((m: Record<string, unknown>) => ({
   403→        id: m.id as string,
   404→        job_id: m.job_id as string,
   405→        worker_id: m.worker_id as string,
   406→        status: m.status as string,
   407→        match_score: m.match_score as number | null,
   408→        ai_summary: m.ai_summary as string | null,
   409→        contractor_action: m.contractor_action as string,
   410→        worker_action: m.worker_action as string,
   411→        hired_at: m.hired_at as string | null,
   412→        fee_charged: m.fee_charged as boolean,
   413→        created_at: m.created_at as string,
   414→        worker: profileMap.get(m.worker_id as string) || null,
   415→        worker_profile: wpMap.get(m.worker_id as string) || null,
   416→      }));
   417→
   418→      setMatches(enriched);
   419→      setLoadingMatches(false);
   420→    }
   421→    loadMatches();
   422→  }, [selectedJobId, supabase, currentPage]);
   423→
   424→  async function handleHire(match: MatchWithWorker) {
   425→    // Haptic feedback
   426→    if (navigator.vibrate) {
   427→      navigator.vibrate(50);
   428→    }
   429→
   430→    if (!companyId) {
   431→      toast.error("Company not found. Please complete your profile.");
   432→      return;
   433→    }
   434→
   435→    // Open payment modal
   436→    setSelectedMatch(match);
   437→    setIsPaymentModalOpen(true);
   438→  }
   439→
   440→  async function handlePaymentSuccess() {
   441→    if (!selectedMatch) return;
   442→
   443→    const supabase = createClient();
   444→    const hiredAt = new Date().toISOString();
   445→
   446→    // CRITICAL: Persist to database BEFORE updating local state
   447→    const { error } = await supabase
   448→      .from("matches")
   449→      .update({
   450→        status: "hired",
   451→        contractor_action: "accepted",
   452→        hired_at: hiredAt,
   453→        fee_charged: true,
   454→      })
   455→      .eq("id", selectedMatch.id);
   456→
   457→    if (error) {
   458→      logger.error("Failed to update match in database:", error);
   459→      alert("Failed to complete hire. Please try again.");
   460→      return;
   461→    }
   462→
   463→    // Update local state to show hired
   464→    setMatches((prev) =>
   465→      prev.map((m) =>
   466→        m.id === selectedMatch.id
   467→          ? { ...m, status: "hired", contractor_action: "accepted", hired_at: hiredAt, fee_charged: true }
   468→          : m
   469→      )
   470→    );
   471→
   472→    setHireSuccess(selectedMatch.id);
   473→    setIsPaymentModalOpen(false);
   474→    setSelectedMatch(null);
   475→    setTimeout(() => setHireSuccess(null), 3000);
   476→  }
   477→
   478→  async function handleReject(match: MatchWithWorker) {
   479→    // Haptic feedback
   480→    if (navigator.vibrate) {
   481→      navigator.vibrate([30, 20, 30]);
   482→    }
   483→
   484→    // Remove from list (soft delete - just update status)
   485→    await supabase
   486→      .from("matches")
   487→      .update({
   488→        contractor_action: "rejected",
   489→      })
   490→      .eq("id", match.id);
   491→
   492→    // Remove from UI
   493→    setMatches((prev) => prev.filter((m) => m.id !== match.id));
   494→  }
   495→
   496→  if (loading) {
   497→    return (
   498→

   499→        <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
   500→

   501→    );
   502→  }
   503→
   504→  return (
   505→

   506→
   507→        {jobs.length === 0 ? (
   508→

   509→            <HardHat className="h-16 w-16 text-muted-foreground/50" />
   510→
No jobs posted yet

   511→

   512→              Post a job to start seeing matched workers.
   513→

   514→            <Button asChild className="mt-8 h-16 px-8 text-lg font-semibold">
   515→              <Link href="/contractor/post-job">Post a Job</Link>
   516→            </Button>
   517→

   518→        ) : (
   519→          <>
   520→            {/* Job selector — horizontal scroll */}
   521→            {jobs.length > 1 && (
   522→

   523→                {jobs.map((job) => (
   524→                  <button
   525→                    key={job.id}
   526→                    onClick={() => {
   527→                      setSelectedJobId(job.id);
   528→                      setCurrentPage(1); // Reset to first page when changing jobs
   529→                    }}
   530→                    className={`shrink-0 rounded-full px-5 py-3 text-base font-semibold transition-all active:scale-95 min-h-[48px] ${
   531→                      selectedJobId === job.id
   532→                        ? "bg-primary text-primary-foreground"
   533→                        : "bg-muted text-muted-foreground"
   534→                    }`}
   535→                  >
   536→                    {job.title}
   537→                  </button>
   538→                ))}
   539→

   540→            )}
   541→
   542→            {/* Selected job summary */}
   543→            {selectedJobId && (
   544→

   545→                {(() => {
   546→                  const job = jobs.find((j) => j.id === selectedJobId);
   547→                  if (!job) return null;
   548→                  return (
   549→

   550→                      <Badge variant="secondary">{TRADE_LABEL[job.trade] || job.trade}</Badge>
   551→                      {job.suburb && (
   552→
   553→                          <MapPin className="h-3.5 w-3.5" />
   554→                          {job.suburb}
   555→
   556→                      )}
   557→
   558→                        {matches.length} match{matches.length !== 1 ? "es" : ""}
   559→
   560→

   561→                  );
   562→                })()}
   563→

   564→            )}
   565→
   566→            {/* Matches */}
   567→            {loadingMatches ? (
   568→

   569→                <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
   570→

   571→            ) : matches.length === 0 ? (
   572→

   573→                <UserCheck className="h-12 w-12 text-muted-foreground/50" />
   574→
No matches yet

   575→

   576→                  Workers will show up here as they apply. We'll notify you.
   577→

   578→

   579→            ) : (
   580→              <>
   581→

   582→                  {/* Swipe hint */}
   583→                  {matches.length > 0 && matches.some(m => m.status !== "hired") && (
   584→

   585→

   586→                        👉 Swipe right to hire • Swipe left to pass
   587→

   588→

   589→                  )}
   590→
   591→                  {matches.map((match) => (
   592→                    <SwipeableMatchCard
   593→                      key={match.id}
   594→                      match={match}
   595→                      isHired={match.status === "hired"}
   596→                      justHired={hireSuccess === match.id}
   597→                      hiring={hiring === match.id}
   598→                      onHire={handleHire}
   599→                      onReject={handleReject}
   600→                    />
   601→                  ))}
   602→

   603→
   604→                {/* Pagination Controls */}
   605→                {totalPages > 1 && (
   606→

   607→                    <Button
   608→                      variant="outline"
   609→                      size="sm"
   610→                      onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
   611→                      disabled={currentPage === 1}
   612→                    >
   613→                      Previous
   614→                    </Button>
   615→
   616→

   617→                      {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
   618→                        let pageNum;
   619→                        if (totalPages <= 5) {
   620→                          pageNum = i + 1;
   621→                        } else if (currentPage <= 3) {
   622→                          pageNum = i + 1;
   623→                        } else if (currentPage >= totalPages - 2) {
   624→                          pageNum = totalPages - 4 + i;
   625→                        } else {
   626→                          pageNum = currentPage - 2 + i;
   627→                        }
   628→
   629→                        return (
   630→                          <Button
   631→                            key={pageNum}
   632→                            variant={currentPage === pageNum ? "default" : "outline"}
   633→                            size="sm"
   634→                            onClick={() => setCurrentPage(pageNum)}
   635→                            className="min-w-[36px]"
   636→                          >
   637→                            {pageNum}
   638→                          </Button>
   639→                        );
   640→                      })}
   641→

   642→
   643→                    <Button
   644→                      variant="outline"
   645→                      size="sm"
   646→                      onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
   647→                      disabled={currentPage === totalPages}
   648→                    >
   649→                      Next
   650→                    </Button>
   651→

   652→                )}
   653→              </>
   654→            )}
   655→          </>
   656→        )}
   657→
   658→
   659→      <BottomNav userType="contractor" />
   660→
   661→      {/* Payment Modal */}
   662→      {selectedMatch && companyId && (
   663→        <HirePaymentModal
   664→          isOpen={isPaymentModalOpen}
   665→          onClose={() => {
   666→            setIsPaymentModalOpen(false);
   667→            setSelectedMatch(null);
   668→          }}
   669→          onSuccess={handlePaymentSuccess}
   670→          matchId={selectedMatch.id}
   671→          companyId={companyId}
   672→          amount={PAYMENT_AMOUNT}
   673→          workerName={selectedMatch.worker?.name || undefined}
   674→        />
   675→      )}
   676→

   677→  );
   678→}
   679→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→'use client';
     2→
     3→import { useEffect, useState } from 'react';
     4→import { useSearchParams, useRouter } from 'next/navigation';
     5→import { logger } from '@/lib/logger';
     6→import Link from 'next/link';
     7→import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
     8→import { Button } from '@/components/ui/button';
     9→import { Card, CardContent } from '@/components/ui/card';
    10→import { createClient } from '@/lib/supabase/client';
    11→
    12→export default function PaymentSuccessPage() {
    13→  const searchParams = useSearchParams();
    14→  const router = useRouter();
    15→  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
    16→  const [message, setMessage] = useState('Confirming your payment...');
    17→  const supabase = createClient();
    18→
    19→  useEffect(() => {
    20→    const paymentIntent = searchParams.get('payment_intent');
    21→    const paymentIntentClientSecret = searchParams.get('payment_intent_client_secret');
    22→
    23→    if (!paymentIntent || !paymentIntentClientSecret) {
    24→      setStatus('error');
    25→      setMessage('Invalid payment confirmation');
    26→      return;
    27→    }
    28→
    29→    // Check payment status
    30→    async function checkPayment() {
    31→      try {
    32→        // Wait a moment for webhook to process
    33→        await new Promise(resolve => setTimeout(resolve, 2000));
    34→
    35→        // Verify payment intent status with Stripe via API
    36→        const response = await fetch(`/api/payments/verify?payment_intent=${paymentIntent}`);
    37→
    38→        if (!response.ok) {
    39→          const error = await response.json();
    40→          setStatus('error');
    41→          setMessage(error.error || 'Failed to verify payment status');
    42→          return;
    43→        }
    44→
    45→        const { status: stripeStatus } = await response.json();
    46→
    47→        // Only show success if payment is actually succeeded
    48→        if (stripeStatus === 'succeeded') {
    49→          // Double-check our database has recorded it
    50→          const { data: payment } = await supabase
    51→            .from('payments')
    52→            .select('status, match_id')
    53→            .eq('stripe_payment_id', paymentIntent)
    54→            .single();
    55→
    56→          if (payment?.status === 'charged') {
    57→            setStatus('success');
    58→            setMessage('Your payment was successful and the worker has been notified!');
    59→          } else {
    60→            // Stripe succeeded but webhook hasn't processed yet
    61→            setStatus('success');
    62→            setMessage('Payment successful! Processing your hire...');
    63→          }
    64→        } else if (stripeStatus === 'processing') {
    65→          setStatus('success');
    66→          setMessage('Payment is being processed. You will be notified once complete.');
    67→        } else if (stripeStatus === 'requires_payment_method') {
    68→          setStatus('error');
    69→          setMessage('Payment failed. Please try again with a different payment method.');
    70→        } else if (stripeStatus === 'canceled') {
    71→          setStatus('error');
    72→          setMessage('Payment was canceled. Please try again.');
    73→        } else {
    74→          setStatus('error');
    75→          setMessage(`Payment status: ${stripeStatus}. Please contact support if you were charged.`);
    76→        }
    77→      } catch (error) {
    78→        logger.error('Error checking payment:', error);
    79→        setStatus('error');
    80→        setMessage('Unable to verify payment status. Please contact support if you were charged.');
    81→      }
    82→    }
    83→
    84→    checkPayment();
    85→  }, [searchParams, supabase]);
    86→
    87→  return (
    88→

    89→      <Card className="w-full max-w-md">
    90→        <CardContent className="pt-8 pb-6 text-center">
    91→          {status === 'loading' && (
    92→            <>
    93→              <Loader2 className="mx-auto h-12 w-12 animate-spin text-primary" />
    94→
Confirming Payment

    95→
{message}

    96→            </>
    97→          )}
    98→
    99→          {status === 'success' && (
   100→            <>
   101→

   102→                <CheckCircle2 className="h-10 w-10 text-green-600" />
   103→

   104→
Payment Successful!

   105→
{message}

   106→

   107→                <Button asChild className="w-full h-12 text-base">
   108→                  <Link href="/contractor/matches">Back to Matches</Link>
   109→                </Button>
   110→                <Button asChild variant="outline" className="w-full h-12 text-base">
   111→                  <Link href="/dashboard">Go to Dashboard</Link>
   112→                </Button>
   113→

   114→            </>
   115→          )}
   116→
   117→          {status === 'error' && (
   118→            <>
   119→

   120→                <AlertCircle className="h-10 w-10 text-destructive" />
   121→

   122→
Payment Error

   123→
{message}

   124→

   125→                <Button asChild variant="outline" className="w-full h-12 text-base">
   126→                  <Link href="/contractor/matches">Back to Matches</Link>
   127→                </Button>
   128→

   129→            </>
   130→          )}
   131→        </CardContent>
   132→      </Card>
   133→

   134→  );
   135→}
   136→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>