feat: add coil-by-coil report, job status by plant, job traveler + PDFs
Implements C-010 (Coil-by-Coil), C-011 (Job Status by Plant), and C-012 (Job Traveler + PDF) completing 12/16 Phase 2 core features. C-010: Job number search → detailed coil-level breakdown showing materials used and produced with independent search/sort/CSV export. Embeds full 782-line CoilByCoil.sql query in service layer. C-011: Auto-loading job status view grouped by plant using OpenOrdersQueryV3 stored procedure. Includes web table with search/sort/CSV export and landscape PDF download. C-012: Multi-section job traveler with header info, shipping schedule, raw materials, operations, and Part_UD paint specifications. Includes job number search page, detail page, and PDF export with Vorteq branding. Deduplicates flat SQL results into structured data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
53c35cc273
commit
1198aaa373
19 changed files with 2712 additions and 18 deletions
40
TASKS.md
40
TASKS.md
|
|
@ -128,7 +128,7 @@
|
||||||
|
|
||||||
## Phase 2: Core Features (Est. 80-110 hrs)
|
## Phase 2: Core Features (Est. 80-110 hrs)
|
||||||
|
|
||||||
**Progress:** 9/16 tasks complete
|
**Progress:** 12/16 tasks complete
|
||||||
|
|
||||||
### C-001: Dashboard
|
### C-001: Dashboard
|
||||||
- [x] Dashboard page at `/(portal)/dashboard/page.tsx`
|
- [x] Dashboard page at `/(portal)/dashboard/page.tsx`
|
||||||
|
|
@ -230,25 +230,33 @@
|
||||||
- **Deps:** F-006, F-009 | **Est:** 4 hrs | **Status:** ✅ Complete
|
- **Deps:** F-006, F-009 | **Est:** 4 hrs | **Status:** ✅ Complete
|
||||||
|
|
||||||
### C-010: Coil-by-Coil Report
|
### C-010: Coil-by-Coil Report
|
||||||
- [ ] `/(portal)/coil-activity/coil-by-coil/page.tsx`
|
- [x] `/(portal)/coil-activity/coil-by-coil/page.tsx` — job number search → coil detail display
|
||||||
- [ ] Job number search input
|
- [x] Job number search input with search button
|
||||||
- [ ] Service: `getCoilByCoil(jobNum, customer)` using `CoilByCoil.sql`
|
- [x] Service: `getCoilByCoil(jobNum, customer)` using full 782-line `CoilByCoil.sql` embedded in service
|
||||||
- [ ] Detail display
|
- [x] Header info card (Job#, Customer, PO, Part, Plant, etc.)
|
||||||
- **Deps:** F-006 | **Est:** 3 hrs
|
- [x] Two-section table: Materials Used + Materials Produced with independent search/sort/CSV export
|
||||||
|
- [x] HDC→HDM customer ID mapping
|
||||||
|
- **Deps:** F-006 | **Est:** 3 hrs | **Status:** ✅ Complete
|
||||||
|
|
||||||
### C-011: Job Status by Plant
|
### C-011: Job Status by Plant
|
||||||
- [ ] `/(portal)/jobs/status/page.tsx`
|
- [x] `/(portal)/jobs/status/page.tsx` — auto-loads on mount
|
||||||
- [ ] Service: `getJobStatusByPlantByCustomer(custId, dbName)` using `JobStatusByPlantByCustomer.sql`
|
- [x] Service: `getJobStatusByPlant(custId)` using `OpenOrdersQueryV3` stored procedure
|
||||||
- [ ] Note: requires DBNAME cross-database parameter
|
- [x] DBNAME cross-database parameter via `getPortalDbName()`
|
||||||
- [ ] Data table grouped by plant
|
- [x] Data table grouped by plant with plant section header rows
|
||||||
- **Deps:** F-006 | **Est:** 3 hrs
|
- [x] Search, sort, CSV export
|
||||||
|
- [x] PDF export: `/api/jobs/status/pdf` (landscape Letter, grouped by plant)
|
||||||
|
- [x] HDC→HDM customer ID mapping
|
||||||
|
- **Deps:** F-006 | **Est:** 3 hrs | **Status:** ✅ Complete
|
||||||
|
|
||||||
### C-012: Job Traveler + PDF
|
### C-012: Job Traveler + PDF
|
||||||
- [ ] `/(portal)/jobs/[jobNum]/traveler/page.tsx`
|
- [x] `/(portal)/jobs/traveler/page.tsx` — job number search page
|
||||||
- [ ] Service: `getJobTraveler(jobNum)` using `jobTraveler.sql`
|
- [x] `/(portal)/jobs/[jobNum]/traveler/page.tsx` — multi-section detail page
|
||||||
- [ ] Detailed routing/operations view
|
- [x] Service: `getJobTraveler(jobNum)` using full `jobTraveler.sql` with Part_UD paint data
|
||||||
- [ ] PDF export
|
- [x] Structured data: header, shipping schedule, raw materials, operations, paint specifications (deduplicated from flat SQL results)
|
||||||
- **Deps:** F-006 | **Est:** 4 hrs
|
- [x] Detail view: Job info card, Schedule dates, Shipping schedule table, Raw materials table, Operations table, Part specifications card
|
||||||
|
- [x] PDF export: `/api/jobs/{jobNum}/traveler/pdf` (landscape Letter, full Vorteq branding)
|
||||||
|
- [x] Sidebar navigation: Job Traveler link under Jobs section
|
||||||
|
- **Deps:** F-006 | **Est:** 4 hrs | **Status:** ✅ Complete
|
||||||
|
|
||||||
### C-013: Shipment Request Cart Workflow
|
### C-013: Shipment Request Cart Workflow
|
||||||
- [ ] `/(portal)/shipment-requests/page.tsx` — list existing requests
|
- [ ] `/(portal)/shipment-requests/page.tsx` — list existing requests
|
||||||
|
|
|
||||||
113
src/app/(portal)/coil-activity/coil-by-coil/page.tsx
Normal file
113
src/app/(portal)/coil-activity/coil-by-coil/page.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import type { CoilByCoilRow } from '@/types/coil-activity';
|
||||||
|
import { CoilByCoilTable } from '@/components/coil-activity/coil-by-coil-table';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[...Array(10)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-12 w-full animate-pulse rounded bg-muted"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CoilByCoilPage() {
|
||||||
|
const [jobNum, setJobNum] = useState('');
|
||||||
|
const [data, setData] = useState<CoilByCoilRow[] | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(async () => {
|
||||||
|
if (!jobNum.trim()) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/coil-activity/coil-by-coil?jobNum=${encodeURIComponent(
|
||||||
|
jobNum.trim()
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(body.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const json = await res.json();
|
||||||
|
setData(json);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [jobNum]);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') handleSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="mb-2 text-3xl font-bold">Coil-by-Coil Report</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
View coil-level material breakdown for a specific job
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6 flex items-center gap-4">
|
||||||
|
<div className="relative max-w-md flex-1">
|
||||||
|
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Enter job number..."
|
||||||
|
value={jobNum}
|
||||||
|
onChange={(e) => setJobNum(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="pl-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleSearch} disabled={!jobNum.trim() || loading}>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 text-center text-destructive">
|
||||||
|
Failed to load coil-by-coil data: {error}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : loading ? (
|
||||||
|
<LoadingSkeleton />
|
||||||
|
) : data !== null ? (
|
||||||
|
data.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 text-center text-muted-foreground">
|
||||||
|
No coil-by-coil data found for job {jobNum}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<CoilByCoilTable data={data} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 text-center text-muted-foreground">
|
||||||
|
Enter a job number to view coil-by-coil details
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
src/app/(portal)/jobs/[jobNum]/traveler/page.tsx
Normal file
76
src/app/(portal)/jobs/[jobNum]/traveler/page.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import type { JobTravelerData } from '@/types/jobs';
|
||||||
|
import { JobTravelerDetail } from '@/components/jobs/job-traveler-detail';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
|
||||||
|
function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[...Array(8)].map((_, i) => (
|
||||||
|
<div key={i} className="h-12 w-full animate-pulse rounded bg-muted" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function JobTravelerDetailPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const jobNum = params.jobNum as string;
|
||||||
|
const [data, setData] = useState<JobTravelerData | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/jobs/${encodeURIComponent(jobNum)}/traveler`);
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(body.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const json = await res.json();
|
||||||
|
setData(json);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [jobNum]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="mb-2 text-3xl font-bold">Job Traveler — {jobNum}</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Detailed job routing and operations information
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 text-center text-destructive">
|
||||||
|
{error.includes('not found')
|
||||||
|
? `No job traveler found for job ${jobNum}`
|
||||||
|
: `Failed to load job traveler: ${error}`}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : loading || data === null ? (
|
||||||
|
<LoadingSkeleton />
|
||||||
|
) : (
|
||||||
|
<JobTravelerDetail data={data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/app/(portal)/jobs/status/page.tsx
Normal file
71
src/app/(portal)/jobs/status/page.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import type { JobStatusRow } from '@/types/jobs';
|
||||||
|
import { JobStatusTable } from '@/components/jobs/job-status-table';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
|
||||||
|
function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[...Array(10)].map((_, i) => (
|
||||||
|
<div key={i} className="h-12 w-full animate-pulse rounded bg-muted" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function JobStatusPage() {
|
||||||
|
const [data, setData] = useState<JobStatusRow[] | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/jobs/status');
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(body.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const json = await res.json();
|
||||||
|
setData(json);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="mb-2 text-3xl font-bold">Job Status</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
View all open jobs grouped by plant
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 text-center text-destructive">
|
||||||
|
Failed to load job status: {error}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : loading || data === null ? (
|
||||||
|
<LoadingSkeleton />
|
||||||
|
) : (
|
||||||
|
<JobStatusTable data={data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
src/app/(portal)/jobs/traveler/page.tsx
Normal file
103
src/app/(portal)/jobs/traveler/page.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import type { JobTravelerData } from '@/types/jobs';
|
||||||
|
import { JobTravelerDetail } from '@/components/jobs/job-traveler-detail';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[...Array(8)].map((_, i) => (
|
||||||
|
<div key={i} className="h-12 w-full animate-pulse rounded bg-muted" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function JobTravelerPage() {
|
||||||
|
const [jobNum, setJobNum] = useState('');
|
||||||
|
const [searchedJobNum, setSearchedJobNum] = useState('');
|
||||||
|
const [data, setData] = useState<JobTravelerData | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(async () => {
|
||||||
|
const trimmed = jobNum.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setSearchedJobNum(trimmed);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/jobs/${encodeURIComponent(trimmed)}/traveler`);
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(body.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const json = await res.json();
|
||||||
|
setData(json);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [jobNum]);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') handleSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="mb-2 text-3xl font-bold">Job Traveler</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
View detailed job routing and operations information
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6 flex items-center gap-4">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Enter job number..."
|
||||||
|
value={jobNum}
|
||||||
|
onChange={(e) => setJobNum(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="pl-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleSearch} disabled={!jobNum.trim() || loading}>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 text-center text-destructive">
|
||||||
|
{error.includes('not found')
|
||||||
|
? `No job traveler found for job ${searchedJobNum}`
|
||||||
|
: `Failed to load job traveler: ${error}`}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : loading ? (
|
||||||
|
<LoadingSkeleton />
|
||||||
|
) : data !== null ? (
|
||||||
|
<JobTravelerDetail data={data} />
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 text-center text-muted-foreground">
|
||||||
|
Enter a job number to view the job traveler
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/app/api/coil-activity/coil-by-coil/route.ts
Normal file
35
src/app/api/coil-activity/coil-by-coil/route.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getQuestSession, getActiveCompany } from '@/lib/permissions';
|
||||||
|
import { getCoilByCoil } from '@/services/coil-activity';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const session = await getQuestSession();
|
||||||
|
const activeCompany = await getActiveCompany();
|
||||||
|
|
||||||
|
if (!session || !activeCompany) {
|
||||||
|
return NextResponse.json({ error: 'No active company' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const jobNum = searchParams.get('jobNum');
|
||||||
|
|
||||||
|
if (!jobNum) {
|
||||||
|
return NextResponse.json({ error: 'jobNum is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getCoilByCoil(jobNum, activeCompany.epicor_cust_id);
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
let details = '';
|
||||||
|
if (err && typeof err === 'object' && 'originalError' in err) {
|
||||||
|
const originalError = (err as { originalError: unknown }).originalError;
|
||||||
|
details = originalError instanceof Error ? originalError.message : String(originalError);
|
||||||
|
}
|
||||||
|
console.error('Error:', err);
|
||||||
|
return NextResponse.json({ error: message, details }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/app/api/jobs/[jobNum]/traveler/pdf/route.ts
Normal file
41
src/app/api/jobs/[jobNum]/traveler/pdf/route.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getJobTraveler } from '@/services/jobs';
|
||||||
|
import { getQuestSession, getActiveCompany } from '@/lib/permissions';
|
||||||
|
import { generatePdfFromHtml } from '@/lib/pdf';
|
||||||
|
import { renderJobTravelerHtml } from '@/lib/pdf-templates/job-traveler';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ jobNum: string }> }
|
||||||
|
) {
|
||||||
|
const { jobNum } = await params;
|
||||||
|
const session = await getQuestSession();
|
||||||
|
const activeCompany = await getActiveCompany();
|
||||||
|
|
||||||
|
if (!session || !activeCompany) {
|
||||||
|
return NextResponse.json({ error: 'No active company' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getJobTraveler(jobNum);
|
||||||
|
if (!data) {
|
||||||
|
return NextResponse.json({ error: 'Job not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = renderJobTravelerHtml(data);
|
||||||
|
const pdfBuffer = await generatePdfFromHtml(html, { format: 'Letter', landscape: true });
|
||||||
|
|
||||||
|
return new NextResponse(new Uint8Array(pdfBuffer), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `attachment; filename="job-traveler-${jobNum}.pdf"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error('Job traveler PDF error:', err);
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/app/api/jobs/[jobNum]/traveler/route.ts
Normal file
38
src/app/api/jobs/[jobNum]/traveler/route.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getQuestSession, getActiveCompany } from '@/lib/permissions';
|
||||||
|
import { getJobTraveler } from '@/services/jobs';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ jobNum: string }> }
|
||||||
|
) {
|
||||||
|
const session = await getQuestSession();
|
||||||
|
const activeCompany = await getActiveCompany();
|
||||||
|
|
||||||
|
if (!session || !activeCompany) {
|
||||||
|
return NextResponse.json({ error: 'No active company' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { jobNum } = await params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getJobTraveler(jobNum);
|
||||||
|
|
||||||
|
if (data === null) {
|
||||||
|
return NextResponse.json({ error: 'Job not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
let details = '';
|
||||||
|
if (err && typeof err === 'object' && 'originalError' in err) {
|
||||||
|
const originalError = (err as { originalError: unknown }).originalError;
|
||||||
|
details = originalError instanceof Error ? originalError.message : String(originalError);
|
||||||
|
}
|
||||||
|
console.error('Error:', err);
|
||||||
|
return NextResponse.json({ error: message, details }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/app/api/jobs/status/pdf/route.ts
Normal file
119
src/app/api/jobs/status/pdf/route.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getJobStatusByPlant } from '@/services/jobs';
|
||||||
|
import { getQuestSession, getActiveCompany } from '@/lib/permissions';
|
||||||
|
import { generatePdfFromHtml } from '@/lib/pdf';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
function fmtDate(iso: string | null): string {
|
||||||
|
if (!iso) return '';
|
||||||
|
const d = new Date(iso);
|
||||||
|
return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}/${d.getFullYear()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJobStatusHtml(data: import('@/types/jobs').JobStatusRow[]): string {
|
||||||
|
// Group by plant
|
||||||
|
const grouped: Record<string, typeof data> = {};
|
||||||
|
for (const row of data) {
|
||||||
|
const p = row.plant_name || 'Unknown';
|
||||||
|
if (!grouped[p]) grouped[p] = [];
|
||||||
|
grouped[p].push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
let plantSections = '';
|
||||||
|
for (const [plant, rows] of Object.entries(grouped)) {
|
||||||
|
const dataRows = rows.map((r, i) => `
|
||||||
|
<tr style="border-top: 1px solid #eee;${i % 2 === 0 ? ' background: #fafafa;' : ''}">
|
||||||
|
<td style="padding: 3px 6px;">${r.job_num}</td>
|
||||||
|
<td style="padding: 3px 6px;">${r.job_released ? 'Yes' : 'No'}</td>
|
||||||
|
<td style="padding: 3px 6px;">${fmtDate(r.due_date)}</td>
|
||||||
|
<td style="padding: 3px 6px;">${r.part_num}</td>
|
||||||
|
<td style="padding: 3px 6px;">${r.customer_part_num || ''}</td>
|
||||||
|
<td style="padding: 3px 6px;">${r.part_description}</td>
|
||||||
|
<td style="padding: 3px 6px; text-align: right;">${r.prod_qty.toLocaleString()}</td>
|
||||||
|
<td style="padding: 3px 6px; text-align: right;">${r.qty_completed.toLocaleString()}</td>
|
||||||
|
<td style="padding: 3px 6px;">${r.order_num ?? ''}</td>
|
||||||
|
<td style="padding: 3px 6px;">${r.order_line ?? ''}</td>
|
||||||
|
<td style="padding: 3px 6px;">${r.order_rel_num ?? ''}</td>
|
||||||
|
<td style="padding: 3px 6px;">${r.po_num || ''}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
plantSections += `
|
||||||
|
<tr>
|
||||||
|
<td colspan="12" style="padding: 6px; background: #e0f2f1; font-weight: bold; color: #0d9488; font-size: 11px;">
|
||||||
|
${plant} (${rows.length} jobs)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
${dataRows}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: Arial, sans-serif; font-size: 9px; color: #333; line-height: 1.3; }
|
||||||
|
table { border-collapse: collapse; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="text-align: center; margin-bottom: 12px;">
|
||||||
|
<div style="font-size: 14px; font-weight: bold; color: #0d9488;">Job Status by Plant</div>
|
||||||
|
<div style="font-size: 10px; color: #666;">Generated ${new Date().toLocaleDateString()}</div>
|
||||||
|
</div>
|
||||||
|
<table style="width: 100%; border: 1px solid #999;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #0d9488; color: white;">
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Job#</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Released</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Due Date</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Part#</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Cust Part#</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Description</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Prod Qty</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Qty Complete</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Order</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Line</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Rel</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">PO#</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${plantSections}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div style="margin-top: 8px; text-align: right; font-size: 8px; color: #999;">
|
||||||
|
Total: ${data.length} jobs across ${Object.keys(grouped).length} plants
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await getQuestSession();
|
||||||
|
const activeCompany = await getActiveCompany();
|
||||||
|
|
||||||
|
if (!session || !activeCompany) {
|
||||||
|
return NextResponse.json({ error: 'No active company' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getJobStatusByPlant(activeCompany.epicor_cust_id);
|
||||||
|
const html = renderJobStatusHtml(data);
|
||||||
|
const pdfBuffer = await generatePdfFromHtml(html, { format: 'Letter', landscape: true });
|
||||||
|
|
||||||
|
return new NextResponse(new Uint8Array(pdfBuffer), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `attachment; filename="job-status-${new Date().toISOString().split('T')[0]}.pdf"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error('Job status PDF error:', err);
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/app/api/jobs/status/route.ts
Normal file
28
src/app/api/jobs/status/route.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getQuestSession, getActiveCompany } from '@/lib/permissions';
|
||||||
|
import { getJobStatusByPlant } from '@/services/jobs';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const session = await getQuestSession();
|
||||||
|
const activeCompany = await getActiveCompany();
|
||||||
|
|
||||||
|
if (!session || !activeCompany) {
|
||||||
|
return NextResponse.json({ error: 'No active company' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getJobStatusByPlant(activeCompany.epicor_cust_id);
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
let details = '';
|
||||||
|
if (err && typeof err === 'object' && 'originalError' in err) {
|
||||||
|
const originalError = (err as { originalError: unknown }).originalError;
|
||||||
|
details = originalError instanceof Error ? originalError.message : String(originalError);
|
||||||
|
}
|
||||||
|
console.error('Error:', err);
|
||||||
|
return NextResponse.json({ error: message, details }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
547
src/components/coil-activity/coil-by-coil-table.tsx
Normal file
547
src/components/coil-activity/coil-by-coil-table.tsx
Normal file
|
|
@ -0,0 +1,547 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import type { CoilByCoilRow } from '@/types/coil-activity';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { Download, Search } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
SortableTableHead,
|
||||||
|
useSortableTable,
|
||||||
|
} from '@/components/ui/sortable-table-head';
|
||||||
|
import { formatDate } from '@/lib/utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: CoilByCoilRow[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CoilByCoilTable({ data }: Props) {
|
||||||
|
const [usedSearchTerm, setUsedSearchTerm] = useState('');
|
||||||
|
const [producedSearchTerm, setProducedSearchTerm] = useState('');
|
||||||
|
|
||||||
|
// Get header info from first row
|
||||||
|
const header = data.length > 0 ? data[0] : null;
|
||||||
|
|
||||||
|
// Split into Used and Produced
|
||||||
|
const usedRows = data.filter((r) => r.tran_type === 'Used');
|
||||||
|
const producedRows = data.filter((r) => r.tran_type === 'Produced');
|
||||||
|
|
||||||
|
// Filter Used rows
|
||||||
|
const filteredUsedRows = usedRows.filter((row) => {
|
||||||
|
if (!usedSearchTerm) return true;
|
||||||
|
const term = usedSearchTerm.toLowerCase();
|
||||||
|
return (
|
||||||
|
row.lot_used?.toLowerCase().includes(term) ||
|
||||||
|
row.part_num_used?.toLowerCase().includes(term) ||
|
||||||
|
row.part_desc_used?.toLowerCase().includes(term) ||
|
||||||
|
row.tran_reference?.toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter Produced rows
|
||||||
|
const filteredProducedRows = producedRows.filter((row) => {
|
||||||
|
if (!producedSearchTerm) return true;
|
||||||
|
const term = producedSearchTerm.toLowerCase();
|
||||||
|
return (
|
||||||
|
row.prod_lot?.toLowerCase().includes(term) ||
|
||||||
|
row.prod_skid?.toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort Used rows
|
||||||
|
const {
|
||||||
|
sortedData: sortedUsedRows,
|
||||||
|
sortKey: usedSortKey,
|
||||||
|
sortDirection: usedSortDirection,
|
||||||
|
handleSort: handleUsedSort,
|
||||||
|
} = useSortableTable(filteredUsedRows, 'lot_used');
|
||||||
|
|
||||||
|
// Sort Produced rows
|
||||||
|
const {
|
||||||
|
sortedData: sortedProducedRows,
|
||||||
|
sortKey: producedSortKey,
|
||||||
|
sortDirection: producedSortDirection,
|
||||||
|
handleSort: handleProducedSort,
|
||||||
|
} = useSortableTable(filteredProducedRows, 'prod_lot');
|
||||||
|
|
||||||
|
// Export Used to CSV
|
||||||
|
const handleExportUsed = () => {
|
||||||
|
const headers = [
|
||||||
|
'Lot#',
|
||||||
|
'Part#',
|
||||||
|
'Description',
|
||||||
|
'Start Wt',
|
||||||
|
'RTS Good',
|
||||||
|
'RTS Reject',
|
||||||
|
'Total Used',
|
||||||
|
'Reference',
|
||||||
|
];
|
||||||
|
const rows = sortedUsedRows.map((row) => [
|
||||||
|
row.lot_used || '',
|
||||||
|
row.part_num_used || '',
|
||||||
|
row.part_desc_used || '',
|
||||||
|
row.start_wt?.toString() || '',
|
||||||
|
row.rts_wt_good?.toString() || '',
|
||||||
|
row.rts_reject?.toString() || '',
|
||||||
|
row.total_wt_used?.toString() || '',
|
||||||
|
row.tran_reference || '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const csv = [headers, ...rows].map((r) => r.join(',')).join('\n');
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `coil-by-coil-used-${header?.job_num || 'export'}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export Produced to CSV
|
||||||
|
const handleExportProduced = () => {
|
||||||
|
const headers = [
|
||||||
|
'Lot#',
|
||||||
|
'Good Wt',
|
||||||
|
'Hold Wt',
|
||||||
|
'Reject Wt',
|
||||||
|
'Theo Wt',
|
||||||
|
'Length',
|
||||||
|
'Skid#',
|
||||||
|
'Date',
|
||||||
|
];
|
||||||
|
const rows = sortedProducedRows.map((row) => [
|
||||||
|
row.prod_lot || '',
|
||||||
|
row.prod_wt_good?.toString() || '',
|
||||||
|
row.prod_wt_hold?.toString() || '',
|
||||||
|
row.prod_wt_reject?.toString() || '',
|
||||||
|
row.prod_theoretical_wt?.toString() || '',
|
||||||
|
row.prod_length?.toString() || '',
|
||||||
|
row.prod_skid || '',
|
||||||
|
row.prod_date ? formatDate(new Date(row.prod_date)) : '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const csv = [headers, ...rows].map((r) => r.join(',')).join('\n');
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `coil-by-coil-produced-${header?.job_num || 'export'}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header Info Card */}
|
||||||
|
{header && (
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm md:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Job #:
|
||||||
|
</span>{' '}
|
||||||
|
{header.job_num}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Customer:
|
||||||
|
</span>{' '}
|
||||||
|
{header.customer_name}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
PO:
|
||||||
|
</span>{' '}
|
||||||
|
{header.cust_po || '-'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Part:
|
||||||
|
</span>{' '}
|
||||||
|
{header.part_num}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Description:
|
||||||
|
</span>{' '}
|
||||||
|
{header.part_description}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Prod Type:
|
||||||
|
</span>{' '}
|
||||||
|
{header.prod_type || '-'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Order/Line:
|
||||||
|
</span>{' '}
|
||||||
|
{header.order_num || '-'}/{header.order_line || '-'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Completion:
|
||||||
|
</span>{' '}
|
||||||
|
{header.completion_date
|
||||||
|
? formatDate(new Date(header.completion_date))
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Ship To:
|
||||||
|
</span>{' '}
|
||||||
|
{header.ship_to_name || '-'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Cust Part:
|
||||||
|
</span>{' '}
|
||||||
|
{header.cust_part || '-'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Plant:
|
||||||
|
</span>{' '}
|
||||||
|
{header.plant_name}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-muted-foreground">
|
||||||
|
Mfg Lot:
|
||||||
|
</span>{' '}
|
||||||
|
{header.mfg_lot || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Materials Used Table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Materials Used</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{sortedUsedRows.length}{' '}
|
||||||
|
{sortedUsedRows.length === 1 ? 'record' : 'records'}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search..."
|
||||||
|
value={usedSearchTerm}
|
||||||
|
onChange={(e) => setUsedSearchTerm(e.target.value)}
|
||||||
|
className="w-64 pl-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleExportUsed}
|
||||||
|
disabled={sortedUsedRows.length === 0}
|
||||||
|
>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-teal-700 hover:bg-teal-700">
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="lot_used"
|
||||||
|
currentSortKey={usedSortKey}
|
||||||
|
currentDirection={usedSortDirection}
|
||||||
|
onSort={handleUsedSort}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
Lot#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="part_num_used"
|
||||||
|
currentSortKey={usedSortKey}
|
||||||
|
currentDirection={usedSortDirection}
|
||||||
|
onSort={handleUsedSort}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
Part#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="part_desc_used"
|
||||||
|
currentSortKey={usedSortKey}
|
||||||
|
currentDirection={usedSortDirection}
|
||||||
|
onSort={handleUsedSort}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="start_wt"
|
||||||
|
currentSortKey={usedSortKey}
|
||||||
|
currentDirection={usedSortDirection}
|
||||||
|
onSort={handleUsedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
Start Wt
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="rts_wt_good"
|
||||||
|
currentSortKey={usedSortKey}
|
||||||
|
currentDirection={usedSortDirection}
|
||||||
|
onSort={handleUsedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
RTS Good
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="rts_reject"
|
||||||
|
currentSortKey={usedSortKey}
|
||||||
|
currentDirection={usedSortDirection}
|
||||||
|
onSort={handleUsedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
RTS Reject
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="total_wt_used"
|
||||||
|
currentSortKey={usedSortKey}
|
||||||
|
currentDirection={usedSortDirection}
|
||||||
|
onSort={handleUsedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
Total Used
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="tran_reference"
|
||||||
|
currentSortKey={usedSortKey}
|
||||||
|
currentDirection={usedSortDirection}
|
||||||
|
onSort={handleUsedSort}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
Reference
|
||||||
|
</SortableTableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{sortedUsedRows.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={8}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No materials used found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
sortedUsedRows.map((row, idx) => (
|
||||||
|
<TableRow
|
||||||
|
key={idx}
|
||||||
|
className={idx % 2 === 0 ? 'bg-muted/50' : ''}
|
||||||
|
>
|
||||||
|
<TableCell>{row.lot_used || '-'}</TableCell>
|
||||||
|
<TableCell>{row.part_num_used || '-'}</TableCell>
|
||||||
|
<TableCell>{row.part_desc_used || '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.start_wt?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.rts_wt_good?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.rts_reject?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.total_wt_used?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{row.tran_reference || '-'}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Materials Produced Table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Materials Produced</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{sortedProducedRows.length}{' '}
|
||||||
|
{sortedProducedRows.length === 1 ? 'record' : 'records'}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search..."
|
||||||
|
value={producedSearchTerm}
|
||||||
|
onChange={(e) => setProducedSearchTerm(e.target.value)}
|
||||||
|
className="w-64 pl-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleExportProduced}
|
||||||
|
disabled={sortedProducedRows.length === 0}
|
||||||
|
>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-teal-700 hover:bg-teal-700">
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="prod_lot"
|
||||||
|
currentSortKey={producedSortKey}
|
||||||
|
currentDirection={producedSortDirection}
|
||||||
|
onSort={handleProducedSort}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
Lot#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="prod_wt_good"
|
||||||
|
currentSortKey={producedSortKey}
|
||||||
|
currentDirection={producedSortDirection}
|
||||||
|
onSort={handleProducedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
Good Wt
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="prod_wt_hold"
|
||||||
|
currentSortKey={producedSortKey}
|
||||||
|
currentDirection={producedSortDirection}
|
||||||
|
onSort={handleProducedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
Hold Wt
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="prod_wt_reject"
|
||||||
|
currentSortKey={producedSortKey}
|
||||||
|
currentDirection={producedSortDirection}
|
||||||
|
onSort={handleProducedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
Reject Wt
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="prod_theoretical_wt"
|
||||||
|
currentSortKey={producedSortKey}
|
||||||
|
currentDirection={producedSortDirection}
|
||||||
|
onSort={handleProducedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
Theo Wt
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="prod_length"
|
||||||
|
currentSortKey={producedSortKey}
|
||||||
|
currentDirection={producedSortDirection}
|
||||||
|
onSort={handleProducedSort}
|
||||||
|
className="text-right text-white"
|
||||||
|
>
|
||||||
|
Length
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="prod_skid"
|
||||||
|
currentSortKey={producedSortKey}
|
||||||
|
currentDirection={producedSortDirection}
|
||||||
|
onSort={handleProducedSort}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
Skid#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="prod_date"
|
||||||
|
currentSortKey={producedSortKey}
|
||||||
|
currentDirection={producedSortDirection}
|
||||||
|
onSort={handleProducedSort}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
Date
|
||||||
|
</SortableTableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{sortedProducedRows.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={8}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No materials produced found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
sortedProducedRows.map((row, idx) => (
|
||||||
|
<TableRow
|
||||||
|
key={idx}
|
||||||
|
className={idx % 2 === 0 ? 'bg-muted/50' : ''}
|
||||||
|
>
|
||||||
|
<TableCell>{row.prod_lot || '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.prod_wt_good?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.prod_wt_hold?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.prod_wt_reject?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.prod_theoretical_wt?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{row.prod_length?.toLocaleString() || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{row.prod_skid || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{row.prod_date
|
||||||
|
? formatDate(new Date(row.prod_date))
|
||||||
|
: '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
214
src/components/jobs/job-status-table.tsx
Normal file
214
src/components/jobs/job-status-table.tsx
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Fragment, useState } from 'react';
|
||||||
|
import type { JobStatusRow } from '@/types/jobs';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { Download, Search } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
SortableTableHead,
|
||||||
|
useSortableTable,
|
||||||
|
} from '@/components/ui/sortable-table-head';
|
||||||
|
import { formatDate } from '@/lib/utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: JobStatusRow[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function JobStatusTable({ data }: Props) {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const filteredData = data.filter((row) => {
|
||||||
|
const s = searchTerm.toLowerCase();
|
||||||
|
return (
|
||||||
|
row.plant_name.toLowerCase().includes(s) ||
|
||||||
|
row.job_num.toLowerCase().includes(s) ||
|
||||||
|
row.part_num.toLowerCase().includes(s) ||
|
||||||
|
(row.customer_part_num || '').toLowerCase().includes(s) ||
|
||||||
|
row.part_description.toLowerCase().includes(s) ||
|
||||||
|
(row.po_num || '').toLowerCase().includes(s)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sortKey, sortDirection, handleSort, sortedData } =
|
||||||
|
useSortableTable(filteredData);
|
||||||
|
|
||||||
|
// Group by plant_name
|
||||||
|
const groupedByPlant: Record<string, JobStatusRow[]> = {};
|
||||||
|
for (const row of sortedData) {
|
||||||
|
const plant = row.plant_name || 'Unknown';
|
||||||
|
if (!groupedByPlant[plant]) groupedByPlant[plant] = [];
|
||||||
|
groupedByPlant[plant].push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExportCSV = () => {
|
||||||
|
const headers = [
|
||||||
|
'Plant',
|
||||||
|
'Job#',
|
||||||
|
'Released',
|
||||||
|
'Due Date',
|
||||||
|
'Part#',
|
||||||
|
'Cust Part#',
|
||||||
|
'Description',
|
||||||
|
'Prod Qty',
|
||||||
|
'Qty Complete',
|
||||||
|
'Order',
|
||||||
|
'Line',
|
||||||
|
'Rel',
|
||||||
|
'PO#',
|
||||||
|
];
|
||||||
|
const rows = sortedData.map((row) => [
|
||||||
|
row.plant_name,
|
||||||
|
row.job_num,
|
||||||
|
row.job_released ? 'Yes' : 'No',
|
||||||
|
row.due_date ? new Date(row.due_date).toLocaleDateString() : '',
|
||||||
|
row.part_num,
|
||||||
|
row.customer_part_num || '',
|
||||||
|
row.part_description,
|
||||||
|
row.prod_qty,
|
||||||
|
row.qty_completed,
|
||||||
|
row.order_num ?? '',
|
||||||
|
row.order_line ?? '',
|
||||||
|
row.order_rel_num ?? '',
|
||||||
|
row.po_num || '',
|
||||||
|
]);
|
||||||
|
const csvContent = [headers, ...rows]
|
||||||
|
.map((row) => row.map((cell) => `"${cell}"`).join(','))
|
||||||
|
.join('\n');
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `job-status-${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Job Status by Plant</CardTitle>
|
||||||
|
<CardDescription>Showing {sortedData.length} open jobs</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="mb-4 flex items-center gap-4">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search jobs, parts, PO numbers..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleExportCSV} variant="outline">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Export CSV
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-teal-700 text-white">
|
||||||
|
<SortableTableHead sortKey="job_num" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Job#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="job_released" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Released
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="due_date" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Due Date
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="part_num" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Part#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="customer_part_num" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Cust Part#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="part_description" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Description
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="prod_qty" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort} className="text-right">
|
||||||
|
Prod Qty
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="qty_completed" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort} className="text-right">
|
||||||
|
Qty Complete
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="order_num" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Order
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="order_line" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Line
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="order_rel_num" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
Rel
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead sortKey="po_num" currentSortKey={sortKey} currentDirection={sortDirection} onSort={handleSort}>
|
||||||
|
PO#
|
||||||
|
</SortableTableHead>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<TableBody>
|
||||||
|
{sortedData.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={12} className="text-center text-muted-foreground">
|
||||||
|
No open jobs found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
// Render grouped by plant
|
||||||
|
Object.entries(groupedByPlant).map(([plant, rows]) => (
|
||||||
|
<Fragment key={plant}>
|
||||||
|
{/* Plant section header */}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={12}
|
||||||
|
className="bg-teal-50 font-semibold text-teal-800"
|
||||||
|
>
|
||||||
|
{plant} ({rows.length} jobs)
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{rows.map((row, i) => (
|
||||||
|
<TableRow key={`${plant}-${i}`} className={i % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||||
|
<TableCell className="font-medium">{row.job_num}</TableCell>
|
||||||
|
<TableCell>{row.job_released ? 'Yes' : 'No'}</TableCell>
|
||||||
|
<TableCell>{row.due_date ? formatDate(new Date(row.due_date)) : '-'}</TableCell>
|
||||||
|
<TableCell>{row.part_num || '-'}</TableCell>
|
||||||
|
<TableCell>{row.customer_part_num || '-'}</TableCell>
|
||||||
|
<TableCell>{row.part_description || '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{row.prod_qty.toLocaleString()}</TableCell>
|
||||||
|
<TableCell className="text-right">{row.qty_completed.toLocaleString()}</TableCell>
|
||||||
|
<TableCell>{row.order_num ?? '-'}</TableCell>
|
||||||
|
<TableCell>{row.order_line ?? '-'}</TableCell>
|
||||||
|
<TableCell>{row.order_rel_num ?? '-'}</TableCell>
|
||||||
|
<TableCell>{row.po_num || '-'}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
|
Showing {sortedData.length} of {data.length} jobs across {Object.keys(groupedByPlant).length} plants
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
270
src/components/jobs/job-traveler-detail.tsx
Normal file
270
src/components/jobs/job-traveler-detail.tsx
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { JobTravelerData } from '@/types/jobs';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
|
import { formatDate } from '@/lib/utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: JobTravelerData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function JobTravelerDetail({ data }: Props) {
|
||||||
|
const { header, shipping_schedule, materials, operations, paint_data } = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Download PDF button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<a href={`/api/jobs/${header.job_num}/traveler/pdf`} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button variant="outline">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download PDF
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Job Header */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Job: {header.job_num}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm md:grid-cols-4">
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Plant:</span> {header.plant}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Customer:</span> {header.customer_name}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">PO Number:</span> {header.po_num || '-'}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Part:</span> {header.part_num}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Description:</span> {header.part_description}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Rev:</span> {header.revision || '-'}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Cust Part:</span> {header.cust_part || '-'}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Warehouse:</span> {header.warehouse || '-'}</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Schedule Dates */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Schedule</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm md:grid-cols-4">
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Total Qty:</span> {header.prod_qty.toLocaleString()} {header.ium}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Start Date:</span> {header.start_date ? formatDate(new Date(header.start_date)) : '-'}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Due Date:</span> {header.due_date ? formatDate(new Date(header.due_date)) : '-'}</div>
|
||||||
|
<div><span className="font-semibold text-muted-foreground">Req. By:</span> {header.req_due_date ? formatDate(new Date(header.req_due_date)) : '-'}</div>
|
||||||
|
</div>
|
||||||
|
{header.comment_text && (
|
||||||
|
<div className="mt-4 rounded-md border bg-muted/30 p-3 text-sm">
|
||||||
|
<span className="font-semibold">Job Comments:</span> {header.comment_text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Shipping Schedule */}
|
||||||
|
{shipping_schedule.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Shipping Schedule</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-hidden rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-teal-700 text-white">
|
||||||
|
<TableHead className="text-white">Date</TableHead>
|
||||||
|
<TableHead className="text-white">Status</TableHead>
|
||||||
|
<TableHead className="text-white">SO</TableHead>
|
||||||
|
<TableHead className="text-white">Line</TableHead>
|
||||||
|
<TableHead className="text-white">Rel</TableHead>
|
||||||
|
<TableHead className="text-right text-white">Order Qty</TableHead>
|
||||||
|
<TableHead className="text-white">UM</TableHead>
|
||||||
|
<TableHead className="text-right text-white">Qty from Job</TableHead>
|
||||||
|
<TableHead className="text-white">Ship Via</TableHead>
|
||||||
|
<TableHead className="text-white">Ship To</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{shipping_schedule.map((line, i) => (
|
||||||
|
<TableRow key={i} className={i % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||||
|
<TableCell>{line.need_by_date ? formatDate(new Date(line.need_by_date)) : '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||||
|
line.line_status === 'Open'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{line.line_status || '-'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{line.order_num ?? '-'}</TableCell>
|
||||||
|
<TableCell>{line.order_line ?? '-'}</TableCell>
|
||||||
|
<TableCell>{line.order_rel_num ?? '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{line.order_qty?.toLocaleString() ?? '-'}</TableCell>
|
||||||
|
<TableCell>{line.sales_um || '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{line.qty_from_job?.toLocaleString() ?? '-'}</TableCell>
|
||||||
|
<TableCell>{line.ship_via || '-'}</TableCell>
|
||||||
|
<TableCell>{line.ship_to || '-'}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Raw Material Components */}
|
||||||
|
{materials.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Raw Material Components</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-hidden rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-teal-700 text-white">
|
||||||
|
<TableHead className="text-white">Seq</TableHead>
|
||||||
|
<TableHead className="text-white">Part#</TableHead>
|
||||||
|
<TableHead className="text-white">Description</TableHead>
|
||||||
|
<TableHead className="text-right text-white">Req Qty</TableHead>
|
||||||
|
<TableHead className="text-white">UM</TableHead>
|
||||||
|
<TableHead className="text-white">Whse</TableHead>
|
||||||
|
<TableHead className="text-white">Rel OP</TableHead>
|
||||||
|
<TableHead className="text-white">Status</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{materials.map((mtl, i) => (
|
||||||
|
<TableRow key={i} className={i % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||||
|
<TableCell>{mtl.mtl_seq}</TableCell>
|
||||||
|
<TableCell>{mtl.part_num || '-'}</TableCell>
|
||||||
|
<TableCell>{mtl.description || '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{mtl.required_qty?.toLocaleString() ?? '-'}</TableCell>
|
||||||
|
<TableCell>{mtl.ium || '-'}</TableCell>
|
||||||
|
<TableCell>{mtl.warehouse_code || '-'}</TableCell>
|
||||||
|
<TableCell>{mtl.related_operation ?? '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||||
|
mtl.issue_status === 'Open'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: mtl.issue_status === 'Issued Complete'
|
||||||
|
? 'bg-gray-100 text-gray-800'
|
||||||
|
: 'bg-blue-100 text-blue-800'
|
||||||
|
}`}>
|
||||||
|
{mtl.issue_status || '-'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Operations */}
|
||||||
|
{operations.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Operations</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-hidden rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-teal-700 text-white">
|
||||||
|
<TableHead className="text-white">Seq</TableHead>
|
||||||
|
<TableHead className="text-white">Code</TableHead>
|
||||||
|
<TableHead className="text-white">Description</TableHead>
|
||||||
|
<TableHead className="text-right text-white">Qty</TableHead>
|
||||||
|
<TableHead className="text-right text-white">Res.</TableHead>
|
||||||
|
<TableHead className="text-right text-white">Setup Hrs</TableHead>
|
||||||
|
<TableHead className="text-right text-white">Prod Hrs</TableHead>
|
||||||
|
<TableHead className="text-right text-white">Standard</TableHead>
|
||||||
|
<TableHead className="text-white">Start</TableHead>
|
||||||
|
<TableHead className="text-white">Due</TableHead>
|
||||||
|
<TableHead className="text-white">Status</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{operations.map((op, i) => (
|
||||||
|
<TableRow key={i} className={i % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||||
|
<TableCell>{op.opr_seq}</TableCell>
|
||||||
|
<TableCell>{op.op_code || '-'}</TableCell>
|
||||||
|
<TableCell>{op.op_desc || '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{op.run_qty?.toLocaleString() ?? '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{op.crew_size ?? '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{op.est_setup_hours?.toFixed(2) ?? '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{op.est_prod_hours?.toFixed(2) ?? '-'}</TableCell>
|
||||||
|
<TableCell className="text-right">{op.prod_standard?.toFixed(2) ?? '-'}</TableCell>
|
||||||
|
<TableCell>{op.start_date ? formatDate(new Date(op.start_date)) : '-'}</TableCell>
|
||||||
|
<TableCell>{op.due_date ? formatDate(new Date(op.due_date)) : '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||||
|
op.status === 'OPEN'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{op.status || '-'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Paint/Part Specifications */}
|
||||||
|
{paint_data && Object.keys(paint_data).length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Part Specifications</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-1 text-sm md:grid-cols-3 lg:grid-cols-4">
|
||||||
|
{Object.entries(paint_data)
|
||||||
|
.filter(([, v]) => v != null && String(v).trim() !== '' && String(v) !== '0' && String(v) !== '0.00000000')
|
||||||
|
.map(([key, value]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<span className="font-semibold text-muted-foreground">{formatPaintKey(key)}:</span>{' '}
|
||||||
|
{String(value)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format paint_data key: remove _c suffix, split on camelCase */
|
||||||
|
function formatPaintKey(key: string): string {
|
||||||
|
return key
|
||||||
|
.replace(/_c$/, '')
|
||||||
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
@ -134,7 +134,10 @@ export function PortalSidebar({
|
||||||
href: '/jobs',
|
href: '/jobs',
|
||||||
icon: <Briefcase className="h-5 w-5" />,
|
icon: <Briefcase className="h-5 w-5" />,
|
||||||
permission: 'view_jobs',
|
permission: 'view_jobs',
|
||||||
children: [{ label: 'Job Status', href: '/jobs/status', icon: null }],
|
children: [
|
||||||
|
{ label: 'Job Status', href: '/jobs/status', icon: null },
|
||||||
|
{ label: 'Job Traveler', href: '/jobs/traveler', icon: null },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
274
src/lib/pdf-templates/job-traveler.ts
Normal file
274
src/lib/pdf-templates/job-traveler.ts
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
import type { JobTravelerData } from '@/types/jobs';
|
||||||
|
|
||||||
|
const VORTEQ_LOGO_SVG = `
|
||||||
|
<svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="50" cy="45" r="35" fill="none" stroke="#8BC34A" stroke-width="8"/>
|
||||||
|
<line x1="65" y1="65" x2="90" y2="90" stroke="#8BC34A" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function fmtDate(iso: string | null): string {
|
||||||
|
if (!iso) return '';
|
||||||
|
const d = new Date(iso);
|
||||||
|
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dd = String(d.getDate()).padStart(2, '0');
|
||||||
|
const yyyy = d.getFullYear();
|
||||||
|
return `${mm}/${dd}/${yyyy}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtQty(qty: number | null | undefined): string {
|
||||||
|
if (qty == null) return '0.00';
|
||||||
|
return qty.toLocaleString('en-US', { minimumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderJobTravelerHtml(data: JobTravelerData): string {
|
||||||
|
const { header, shipping_schedule, materials, operations, paint_data } = data;
|
||||||
|
|
||||||
|
// Build shipping schedule rows
|
||||||
|
const shippingRows = shipping_schedule.map((s, i) => `
|
||||||
|
<tr style="border-top: 1px solid #ddd;${i % 2 === 0 ? ' background: #fafafa;' : ''}">
|
||||||
|
<td style="padding: 4px 6px;">${fmtDate(s.need_by_date)}</td>
|
||||||
|
<td style="padding: 4px 6px;">${s.line_status || ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${s.order_num || ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${s.order_line ?? ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${s.order_rel_num ?? ''}</td>
|
||||||
|
<td style="padding: 4px 6px; text-align: right;">${fmtQty(s.order_qty)}</td>
|
||||||
|
<td style="padding: 4px 6px;">${s.sales_um || ''}</td>
|
||||||
|
<td style="padding: 4px 6px; text-align: right;">${fmtQty(s.qty_from_job)}</td>
|
||||||
|
<td style="padding: 4px 6px;">${s.ship_via || ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${s.ship_to || ''}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Build materials rows
|
||||||
|
const materialsRows = materials.map((m, i) => `
|
||||||
|
<tr style="border-top: 1px solid #ddd;${i % 2 === 0 ? ' background: #fafafa;' : ''}">
|
||||||
|
<td style="padding: 4px 6px;">${m.mtl_seq ?? ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${m.part_num || ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${m.description || ''}</td>
|
||||||
|
<td style="padding: 4px 6px; text-align: right;">${fmtQty(m.required_qty)}</td>
|
||||||
|
<td style="padding: 4px 6px;">${m.ium || ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${m.warehouse_code || ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${m.related_operation ?? ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${m.issue_status || ''}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Build operations rows
|
||||||
|
const operationsRows = operations.map((op, i) => `
|
||||||
|
<tr style="border-top: 1px solid #ddd;${i % 2 === 0 ? ' background: #fafafa;' : ''}">
|
||||||
|
<td style="padding: 4px 6px;">${op.opr_seq ?? ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${op.op_code || ''}</td>
|
||||||
|
<td style="padding: 4px 6px;">${op.op_desc || ''}</td>
|
||||||
|
<td style="padding: 4px 6px; text-align: right;">${fmtQty(op.run_qty)}</td>
|
||||||
|
<td style="padding: 4px 6px;">${op.crew_size || ''}</td>
|
||||||
|
<td style="padding: 4px 6px; text-align: right;">${op.est_setup_hours?.toFixed(2) || '0.00'}</td>
|
||||||
|
<td style="padding: 4px 6px; text-align: right;">${op.est_prod_hours?.toFixed(2) || '0.00'}</td>
|
||||||
|
<td style="padding: 4px 6px; text-align: right;">${op.prod_standard?.toFixed(4) || '0.0000'}</td>
|
||||||
|
<td style="padding: 4px 6px;">${fmtDate(op.start_date)}</td>
|
||||||
|
<td style="padding: 4px 6px;">${fmtDate(op.due_date)}</td>
|
||||||
|
<td style="padding: 4px 6px;">${op.status || ''}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Build part specifications grid
|
||||||
|
const specs = paint_data || {};
|
||||||
|
const specFields = [
|
||||||
|
{ label: 'Part Color', key: 'ShortChar01' },
|
||||||
|
{ label: 'Paint Type', key: 'ShortChar02' },
|
||||||
|
{ label: 'Substrate', key: 'ShortChar03' },
|
||||||
|
{ label: 'Gloss Level', key: 'ShortChar04' },
|
||||||
|
{ label: 'Bend Dir', key: 'ShortChar05' },
|
||||||
|
{ label: 'Texture', key: 'ShortChar06' },
|
||||||
|
{ label: 'Finish', key: 'ShortChar07' },
|
||||||
|
{ label: 'Coverage', key: 'ShortChar08' },
|
||||||
|
{ label: 'Adhesion', key: 'ShortChar09' },
|
||||||
|
{ label: 'QC Notes', key: 'ShortChar10' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const specRows = specFields
|
||||||
|
.filter(f => specs[f.key as keyof typeof specs])
|
||||||
|
.map(f => `
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 8px; font-weight: bold; border-right: 1px solid #ccc; background: #f5f5f5;">${f.label}</td>
|
||||||
|
<td style="padding: 4px 8px;">${specs[f.key as keyof typeof specs] || ''}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const specsSection = specRows ? `
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<div style="font-weight: bold; font-size: 11px; margin-bottom: 4px; color: #0d9488;">Part Specifications</div>
|
||||||
|
<table style="width: 50%; border: 1px solid #999; border-collapse: collapse;">
|
||||||
|
${specRows}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: Arial, sans-serif; font-size: 9px; color: #333; padding: 12px; line-height: 1.3; }
|
||||||
|
table { border-collapse: collapse; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 12px; border-bottom: 2px solid #0d9488; padding-bottom: 8px;">
|
||||||
|
<div>
|
||||||
|
<div style="margin-bottom: 4px;">${VORTEQ_LOGO_SVG}</div>
|
||||||
|
<div style="font-size: 8px; color: #666;">
|
||||||
|
<div>1234 Industrial Way</div>
|
||||||
|
<div>Manufacturing City, ST 12345</div>
|
||||||
|
<div>Phone: (555) 123-4567</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; flex: 1;">
|
||||||
|
<div style="font-size: 18px; font-weight: bold; color: #0d9488; margin-bottom: 4px;">JOB TRAVELER</div>
|
||||||
|
<div style="font-size: 11px;">
|
||||||
|
<strong>Job #:</strong> ${header.job_num} <strong>Plant:</strong> ${header.plant || ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 16px; margin-bottom: 12px;">
|
||||||
|
<div style="flex: 2;">
|
||||||
|
<table style="width: 100%; border: 1px solid #999;">
|
||||||
|
<tr style="background: #0d9488; color: white;">
|
||||||
|
<th colspan="2" style="padding: 4px 6px; text-align: left; font-size: 10px;">Job Information</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; width: 30%; background: #f5f5f5;">Customer</td>
|
||||||
|
<td style="padding: 4px 6px;">${header.customer_name || ''}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">PO Number</td>
|
||||||
|
<td style="padding: 4px 6px;">${header.po_num || ''}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">Part Number</td>
|
||||||
|
<td style="padding: 4px 6px;">${header.part_num || ''}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">Description</td>
|
||||||
|
<td style="padding: 4px 6px;">${header.part_description || ''}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">Revision</td>
|
||||||
|
<td style="padding: 4px 6px;">${header.revision || ''}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">Customer Part</td>
|
||||||
|
<td style="padding: 4px 6px;">${header.cust_part || ''}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<table style="width: 100%; border: 1px solid #999;">
|
||||||
|
<tr style="background: #0d9488; color: white;">
|
||||||
|
<th colspan="2" style="padding: 4px 6px; text-align: left; font-size: 10px;">Schedule</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">Total Qty</td>
|
||||||
|
<td style="padding: 4px 6px; text-align: right;">${fmtQty(header.prod_qty)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">Start Date</td>
|
||||||
|
<td style="padding: 4px 6px;">${fmtDate(header.start_date)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">Due Date</td>
|
||||||
|
<td style="padding: 4px 6px;">${fmtDate(header.due_date)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 6px; font-weight: bold; background: #f5f5f5;">Req Date</td>
|
||||||
|
<td style="padding: 4px 6px;">${fmtDate(header.req_due_date)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${header.comment_text ? `
|
||||||
|
<div style="margin-bottom: 12px; padding: 6px; border: 1px solid #999; background: #fffef0;">
|
||||||
|
<div style="font-weight: bold; font-size: 10px; color: #0d9488; margin-bottom: 2px;">Job Comments</div>
|
||||||
|
<div style="white-space: pre-wrap;">${header.comment_text}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<table style="width: 100%; border: 1px solid #999;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #0d9488; color: white;">
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Date</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Status</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">SO</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Line</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Rel</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Order Qty</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">UM</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Qty From Job</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Ship Via</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Ship To</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${shippingRows || '<tr><td colspan="10" style="padding: 8px; text-align: center; color: #999;">No shipping schedule</td></tr>'}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="font-weight: bold; font-size: 10px; margin-bottom: 4px; color: #0d9488;">Raw Material Components</div>
|
||||||
|
<table style="width: 100%; border: 1px solid #999;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #0d9488; color: white;">
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Seq</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Part Number</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Description</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Req Qty</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">UM</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Whse</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Rel OP</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${materialsRows || '<tr><td colspan="8" style="padding: 8px; text-align: center; color: #999;">No materials</td></tr>'}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="font-weight: bold; font-size: 10px; margin-bottom: 4px; color: #0d9488;">Operations</div>
|
||||||
|
<table style="width: 100%; border: 1px solid #999;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #0d9488; color: white;">
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Seq</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Code</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Description</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Qty</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Res</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Setup Hrs</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Prod Hrs</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: right;">Standard</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Start</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Due</th>
|
||||||
|
<th style="padding: 4px 6px; text-align: left;">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${operationsRows || '<tr><td colspan="11" style="padding: 8px; text-align: center; color: #999;">No operations</td></tr>'}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${specsSection}
|
||||||
|
|
||||||
|
<div style="margin-top: 16px; font-size: 8px; color: #999; text-align: right;">
|
||||||
|
Generated ${new Date().toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execQuery } from '@/lib/epicor';
|
import { execQuery } from '@/lib/epicor';
|
||||||
import type { CoilUsageRow, CoilReceiptRow } from '@/types/coil-activity';
|
import type { CoilUsageRow, CoilReceiptRow, CoilByCoilRow } from '@/types/coil-activity';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Coil Usage
|
// Coil Usage
|
||||||
|
|
@ -256,3 +256,306 @@ function mapCoilReceiptRow(raw: Record<string, unknown>): CoilReceiptRow {
|
||||||
mill_order_num: raw.MillOrderNum ? String(raw.MillOrderNum) : null,
|
mill_order_num: raw.MillOrderNum ? String(raw.MillOrderNum) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Coil-by-Coil
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get coil-by-coil tracking data for a specific job and customer.
|
||||||
|
* Returns detailed production and usage data for each coil lot.
|
||||||
|
*
|
||||||
|
* HDC customer exception: maps 'HDC' → 'HDM' for the query.
|
||||||
|
*/
|
||||||
|
export async function getCoilByCoil(jobNum: string, custId: string): Promise<CoilByCoilRow[]> {
|
||||||
|
// HDC → HDM mapping
|
||||||
|
const queryCustId = custId === 'HDC' ? 'HDM' : custId;
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
PartLot.MfgLot,
|
||||||
|
[Company].[Name] [Company_Name],
|
||||||
|
Erp.Plant.Name AS [Calculated_PlantName],
|
||||||
|
[JobHead].[JobNum] [JobHead_JobNum],
|
||||||
|
[Customer].[Name] [Customer_Name],
|
||||||
|
[ShipTo].[Name] [ShipTo_Name],
|
||||||
|
(CASE WHEN [OrderHed].[PONum] <> ' ' THEN [OrderHed].[PONum]
|
||||||
|
ELSE [SubQuery2].[OrderHed1_PONum]
|
||||||
|
END) [Calculated_CustPo],
|
||||||
|
(CASE WHEN [OrderDtl].[XPartNum] > ' ' THEN [OrderDtl].[XPartNum]
|
||||||
|
ELSE [CustXPrt].[XPartNum]
|
||||||
|
END) [Calculated_CustPart],
|
||||||
|
[JobHead].[PartNum] [JobHead_PartNum],
|
||||||
|
[JobHead].[PartDescription] [JobHead_PartDescription],
|
||||||
|
(CASE WHEN [JobHead].[PartNum] = [PartPlant].[PartNum]
|
||||||
|
THEN (CASE WHEN [PartPlant_UD].[RcvType_c] = 'BOS' THEN 'BOS'
|
||||||
|
ELSE (CASE WHEN [PartPlant_UD].[RcvType_c] = 'BOP' THEN 'BOP' ELSE ' ' END)
|
||||||
|
END)
|
||||||
|
END) [Calculated_ProdType],
|
||||||
|
[JobProd].[OrderNum] [JobProd_OrderNum],
|
||||||
|
[JobProd].[OrderLine] [JobProd_OrderLine],
|
||||||
|
[JobHead].[JobCompletionDate] [JobHead_JobCompletionDate],
|
||||||
|
(CASE WHEN [PartTran].[TranType] = 'STK-MTL' AND [JobHead].[JobNum] = [PartTran].[JobNum]
|
||||||
|
THEN [PartTran].[PartNum] ELSE ' '
|
||||||
|
END) [Calculated_PartNumUsed],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used' THEN [PartTran].[PartDescription] ELSE ' '
|
||||||
|
END) [Calculated_PartDescUsed],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used' THEN [PartTran].[LotNum] ELSE ' '
|
||||||
|
END) [Calculated_LotUsed],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used' AND [PartTran].[TranQty] > 0 THEN [PartTran].[TranQty]
|
||||||
|
ELSE (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used' AND [PartLot_UD].[MasterLotNum_c] = [PartTran].[LotNum]
|
||||||
|
THEN [PartTran].[TranQty] ELSE '0'
|
||||||
|
END)
|
||||||
|
END) [Calculated_StartWt],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used' AND [PartTran].[TranQty] < 0
|
||||||
|
AND (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used'
|
||||||
|
AND [PartTran].[WareHouseCode] = '12' OR [PartTran].[WareHouseCode] = '22'
|
||||||
|
OR [PartTran].[WareHouseCode] = '32' OR [PartTran].[WareHouseCode] = '42'
|
||||||
|
OR [PartTran].[WareHouseCode] = '52' OR [PartTran].[WareHouseCode] = '62'
|
||||||
|
THEN ABS([PartTran].[TranQty]) ELSE '0'
|
||||||
|
END) = 0 THEN ABS([PartTran].[TranQty])
|
||||||
|
ELSE '0'
|
||||||
|
END) [Calculated_RtsWtGood],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used'
|
||||||
|
AND [PartTran].[WareHouseCode] = '12' OR [PartTran].[WareHouseCode] = '22'
|
||||||
|
OR [PartTran].[WareHouseCode] = '32' OR [PartTran].[WareHouseCode] = '42'
|
||||||
|
OR [PartTran].[WareHouseCode] = '52' OR [PartTran].[WareHouseCode] = '62'
|
||||||
|
THEN ABS([PartTran].[TranQty]) ELSE '0'
|
||||||
|
END) [Calculated_RtsReject],
|
||||||
|
[PartTran].[TranReference] [PartTran_TranReference],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used' AND [PartTran].[TranQty] > 0 THEN [PartTran].[TranQty]
|
||||||
|
ELSE (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used' AND [PartLot_UD].[MasterLotNum_c] = [PartTran].[LotNum]
|
||||||
|
THEN [PartTran].[TranQty] ELSE '0'
|
||||||
|
END)
|
||||||
|
END)
|
||||||
|
- (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used' AND [PartTran].[TranQty] < 0
|
||||||
|
AND (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used'
|
||||||
|
AND [PartTran].[WareHouseCode] = '12' OR [PartTran].[WareHouseCode] = '22'
|
||||||
|
OR [PartTran].[WareHouseCode] = '32' OR [PartTran].[WareHouseCode] = '42'
|
||||||
|
OR [PartTran].[WareHouseCode] = '52' OR [PartTran].[WareHouseCode] = '62'
|
||||||
|
THEN ABS([PartTran].[TranQty]) ELSE '0'
|
||||||
|
END) = 0 THEN ABS([PartTran].[TranQty])
|
||||||
|
ELSE '0'
|
||||||
|
END)
|
||||||
|
- (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Used'
|
||||||
|
AND [PartTran].[WareHouseCode] = '12' OR [PartTran].[WareHouseCode] = '22'
|
||||||
|
OR [PartTran].[WareHouseCode] = '32' OR [PartTran].[WareHouseCode] = '42'
|
||||||
|
OR [PartTran].[WareHouseCode] = '52' OR [PartTran].[WareHouseCode] = '62'
|
||||||
|
THEN ABS([PartTran].[TranQty]) ELSE '0'
|
||||||
|
END) [Calculated_TotalWtUsed],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Produced'
|
||||||
|
AND [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c]
|
||||||
|
AND [PartTran].[LotNum] = [PartLot].[LotNum]
|
||||||
|
THEN (CASE WHEN ([UD01].[CheckBox01] = 0 AND [UD01].[CheckBox02] = 0)
|
||||||
|
AND ([PartPlant_UD].[RcvType_c] = 'BOP')
|
||||||
|
THEN ([PartTran].[LotNum] + '#')
|
||||||
|
ELSE [PartTran].[LotNum]
|
||||||
|
END)
|
||||||
|
END) [Calculated_ProdLot],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Produced'
|
||||||
|
THEN (CASE WHEN [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c]
|
||||||
|
THEN (CASE WHEN [UD01].[CheckBox01] = 0 AND [UD01].[CheckBox02] = 0
|
||||||
|
THEN [PartLot_UD].[CoilWeight_c] ELSE 0
|
||||||
|
END)
|
||||||
|
END)
|
||||||
|
END) [Calculated_ProdGoodWt],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Produced'
|
||||||
|
THEN (CASE WHEN [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c]
|
||||||
|
THEN (CASE WHEN [UD01].[CheckBox01] = 1 THEN [PartLot_UD].[CoilWeight_c] ELSE '0' END)
|
||||||
|
END)
|
||||||
|
END) [Calculated_HoldWt],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Produced'
|
||||||
|
THEN (CASE WHEN [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c]
|
||||||
|
THEN (CASE WHEN [UD01].[CheckBox02] = 1 THEN [PartLot_UD].[CoilWeight_c] ELSE '0' END)
|
||||||
|
END)
|
||||||
|
END) [Calculated_RejWt],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Produced'
|
||||||
|
THEN (CASE WHEN [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c]
|
||||||
|
THEN [PartLot_UD].[TheoreticalWeight_c] ELSE NULL
|
||||||
|
END)
|
||||||
|
END) [Calculated_TheoWt],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Produced'
|
||||||
|
AND [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c]
|
||||||
|
THEN [PartLot_UD].[Length_c] ELSE NULL
|
||||||
|
END) [Calculated_LinealFt],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Produced'
|
||||||
|
AND [JobHead].[JobNum] = [PartTran].[JobNum]
|
||||||
|
AND [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c]
|
||||||
|
THEN [PartLot_UD].[SkidNum_c] ELSE ' '
|
||||||
|
END) [Calculated_SkidNum],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) = 'Produced'
|
||||||
|
AND [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c]
|
||||||
|
THEN [PartTran].[TranDate] ELSE NULL
|
||||||
|
END) [Calculated_ProdDate],
|
||||||
|
(CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced'
|
||||||
|
ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END)
|
||||||
|
END) [Calculated_ProdOrUsed],
|
||||||
|
[PartTran].[LotNum] [PartTran_LotNum],
|
||||||
|
[PartTran].[TranQty] [PartTran_TranQty]
|
||||||
|
FROM [Erp].[JobHead]
|
||||||
|
LEFT OUTER JOIN [Erp].[JobProd] ON [Erp].[JobHead].[Company] = [Erp].[JobProd].[Company] AND [Erp].[JobHead].[JobNum] = [Erp].[JobProd].[JobNum]
|
||||||
|
LEFT OUTER JOIN [Erp].[OrderHed] ON [Erp].[OrderHed].[Company] = [Erp].[JobProd].[Company] AND [Erp].[OrderHed].[OrderNum] = [Erp].[JobProd].[OrderNum]
|
||||||
|
LEFT OUTER JOIN [Erp].[Part] ON [Erp].[JobHead].[Company] = [Erp].[Part].[Company] AND [Erp].[JobHead].[PartNum] = [Erp].[Part].[PartNum]
|
||||||
|
LEFT OUTER JOIN [Erp].[Customer] ON [Erp].[Part].[Company] = [Erp].[Customer].[Company]
|
||||||
|
LEFT OUTER JOIN [Erp].[CustXPrt] ON [Erp].[JobHead].[Company] = [Erp].[CustXPrt].[Company] AND [Erp].[JobHead].[PartNum] = [Erp].[CustXPrt].[PartNum]
|
||||||
|
LEFT OUTER JOIN [Erp].[ShipTo] ON [Erp].[OrderHed].[Company] = [Erp].[ShipTo].[Company] AND [Erp].[OrderHed].[CustNum] = [Erp].[ShipTo].[CustNum] AND [Erp].[OrderHed].[ShipToNum] = [Erp].[ShipTo].[ShipToNum]
|
||||||
|
LEFT OUTER JOIN [Erp].[Company] ON [Erp].[Company].[Company] = [Erp].[JobHead].[Company]
|
||||||
|
LEFT OUTER JOIN [Erp].[OrderDtl] ON [Erp].[JobProd].[Company] = [Erp].[OrderDtl].[Company] AND [Erp].[JobProd].[OrderNum] = [Erp].[OrderDtl].[OrderNum] AND [Erp].[JobProd].[OrderLine] = [Erp].[OrderDtl].[OrderLine]
|
||||||
|
LEFT OUTER JOIN (
|
||||||
|
SELECT [JobHead1].[Plant] [JobHead1_Plant], [JobHead1].[JobNum] [JobHead1_JobNum], [JobProd1].[JobNum] [JobProd1_JobNum],
|
||||||
|
[JobProd1].[OrderLine] [JobProd1_OrderLine], [JobProd1].[OrderNum] [JobProd1_OrderNum], [JobProd1].[OrderRelNum] [JobProd1_OrderRelNum],
|
||||||
|
[JobProd1].[Plant] [JobProd1_Plant], [OrderHed1].[PONum] [OrderHed1_PONum],
|
||||||
|
[JobHead1].[PartNum] [JobHead1_PartNum], [JobHead1].[PartDescription] [JobHead1_PartDescription]
|
||||||
|
FROM [Erp].[JobHead] [JobHead1]
|
||||||
|
INNER JOIN [Erp].[JobProd] [JobProd1] ON [JobHead1].[Company] = [JobProd1].[Company] AND [JobHead1].[JobNum] = [JobProd1].[JobNum]
|
||||||
|
INNER JOIN [Erp].[OrderHed] [OrderHed1] ON [JobProd1].[Company] = [OrderHed1].[Company] AND [JobProd1].[OrderNum] = [OrderHed1].[OrderNum]
|
||||||
|
) [SubQuery2] ON [Erp].[JobProd].[TargetJobNum] = [SubQuery2].[JobHead1_JobNum]
|
||||||
|
LEFT OUTER JOIN ([Erp].[PartPlant] INNER JOIN [Erp].[PartPlant_UD] ON [Erp].[PartPlant].[SysRowID] = [Erp].[PartPlant_UD].[ForeignSysRowID])
|
||||||
|
ON [Erp].[PartPlant].[Company] = [Erp].[JobHead].[Company] AND [Erp].[PartPlant].[Plant] = [Erp].[JobHead].[Plant] AND [Erp].[PartPlant].[PartNum] = [Erp].[JobHead].[PartNum]
|
||||||
|
LEFT OUTER JOIN [Erp].[PartTran] ON ([Erp].[JobHead].[Company] = [Erp].[PartTran].[Company] AND [Erp].[JobHead].[JobNum] = [Erp].[PartTran].[JobNum] AND [Erp].[JobHead].[Plant] = [Erp].[PartTran].[Plant] AND [Erp].[JobHead].[IUM] = [Erp].[PartTran].[UM])
|
||||||
|
AND ([PartTran].[TranType] = N'STK-MTL' OR [PartTran].[TranType] = N'MFG-STK' AND NOT ([PartTran].[UM] = N'GA'))
|
||||||
|
LEFT OUTER JOIN ([Erp].[PartLot] INNER JOIN [Erp].[PartLot_UD] ON [Erp].[PartLot].[SysRowID] = [Erp].[PartLot_UD].[ForeignSysRowID])
|
||||||
|
ON [Erp].[PartTran].[Company] = [Erp].[PartLot].[Company] AND [Erp].[PartTran].[PartNum] = [Erp].[PartLot].[PartNum] AND [Erp].[PartTran].[LotNum] = [Erp].[PartLot].[LotNum]
|
||||||
|
LEFT OUTER JOIN [Ice].[UD01] ON [Erp].[PartLot].[Company] = [Ice].[UD01].[Company] AND [Erp].[PartLot].[PartNum] = [Ice].[UD01].[ShortChar02] AND [Erp].[PartLot_UD].[SkidNum_c] = [Ice].[UD01].[Key1]
|
||||||
|
JOIN Erp.Plant ON Erp.Plant.Plant = [JobHead].[Plant]
|
||||||
|
WHERE [JobHead].[IUM] = N'LB'
|
||||||
|
AND [JobHead].[JobNum] = @JobNum
|
||||||
|
AND ([Customer].[CustID] = @Customer OR ('HDM' = @Customer2 AND [Customer].[CustID] = 'HDC' AND [JobHead].[JobNum] = @JobNum2))
|
||||||
|
AND ([PartPlant].[Plant] IS NULL
|
||||||
|
OR EXISTS (SELECT * FROM (SELECT 1 [c1] FROM [Ice].[SysUserComp] [SysUserComp]
|
||||||
|
WHERE [SysUserComp].[Company] = [PartPlant].[Company]
|
||||||
|
AND Ice.lookup(PartPlant.Plant, SysUserComp.PlantList, '~') > 0) _PlantPartPlantInner))
|
||||||
|
GROUP BY [Company].[Name], Erp.Plant.Name, [JobHead].[JobNum], [Customer].[Name], [ShipTo].[Name],
|
||||||
|
(CASE WHEN [OrderHed].[PONum] <> ' ' THEN [OrderHed].[PONum] ELSE [SubQuery2].[OrderHed1_PONum] END),
|
||||||
|
(CASE WHEN [OrderDtl].[XPartNum] > ' ' THEN [OrderDtl].[XPartNum] ELSE [CustXPrt].[XPartNum] END),
|
||||||
|
[JobHead].[PartNum], [JobHead].[PartDescription],
|
||||||
|
(CASE WHEN [JobHead].[PartNum] = [PartPlant].[PartNum] THEN (CASE WHEN [PartPlant_UD].[RcvType_c] = 'BOS' THEN 'BOS' ELSE (CASE WHEN [PartPlant_UD].[RcvType_c] = 'BOP' THEN 'BOP' ELSE ' ' END) END) END),
|
||||||
|
[JobProd].[OrderNum], [JobProd].[OrderLine], [JobHead].[JobCompletionDate],
|
||||||
|
(CASE WHEN [PartTran].[TranType] = 'STK-MTL' AND [JobHead].[JobNum] = [PartTran].[JobNum] THEN [PartTran].[PartNum] ELSE ' ' END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' THEN [PartTran].[PartDescription] ELSE ' ' END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' THEN [PartTran].[LotNum] ELSE ' ' END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartTran].[TranQty] > 0 THEN [PartTran].[TranQty] ELSE (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartLot_UD].[MasterLotNum_c] = [PartTran].[LotNum] THEN [PartTran].[TranQty] ELSE '0' END) END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartTran].[TranQty] < 0 AND (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartTran].[WareHouseCode] = '12' OR [PartTran].[WareHouseCode] = '22' OR [PartTran].[WareHouseCode] = '32' OR [PartTran].[WareHouseCode] = '42' OR [PartTran].[WareHouseCode] = '52' OR [PartTran].[WareHouseCode] = '62' THEN ABS([PartTran].[TranQty]) ELSE '0' END) = 0 THEN ABS([PartTran].[TranQty]) ELSE '0' END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartTran].[WareHouseCode] = '12' OR [PartTran].[WareHouseCode] = '22' OR [PartTran].[WareHouseCode] = '32' OR [PartTran].[WareHouseCode] = '42' OR [PartTran].[WareHouseCode] = '52' OR [PartTran].[WareHouseCode] = '62' THEN ABS([PartTran].[TranQty]) ELSE '0' END),
|
||||||
|
[PartTran].[TranReference],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartTran].[TranQty] > 0 THEN [PartTran].[TranQty] ELSE (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartLot_UD].[MasterLotNum_c] = [PartTran].[LotNum] THEN [PartTran].[TranQty] ELSE '0' END) END)
|
||||||
|
- (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartTran].[TranQty] < 0 AND (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartTran].[WareHouseCode] = '12' OR [PartTran].[WareHouseCode] = '22' OR [PartTran].[WareHouseCode] = '32' OR [PartTran].[WareHouseCode] = '42' OR [PartTran].[WareHouseCode] = '52' OR [PartTran].[WareHouseCode] = '62' THEN ABS([PartTran].[TranQty]) ELSE '0' END) = 0 THEN ABS([PartTran].[TranQty]) ELSE '0' END)
|
||||||
|
- (CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Used' AND [PartTran].[WareHouseCode] = '12' OR [PartTran].[WareHouseCode] = '22' OR [PartTran].[WareHouseCode] = '32' OR [PartTran].[WareHouseCode] = '42' OR [PartTran].[WareHouseCode] = '52' OR [PartTran].[WareHouseCode] = '62' THEN ABS([PartTran].[TranQty]) ELSE '0' END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Produced' AND [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c] AND [PartTran].[LotNum] = [PartLot].[LotNum] THEN (CASE WHEN ([UD01].[CheckBox01] = 0 AND [UD01].[CheckBox02] = 0) AND ([PartPlant_UD].[RcvType_c] = 'BOP') THEN ([PartTran].[LotNum] + '#') ELSE [PartTran].[LotNum] END) END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Produced' THEN (CASE WHEN [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c] THEN (CASE WHEN [UD01].[CheckBox01] = 0 AND [UD01].[CheckBox02] = 0 THEN [PartLot_UD].[CoilWeight_c] ELSE 0 END) END) END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Produced' THEN (CASE WHEN [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c] THEN (CASE WHEN [UD01].[CheckBox01] = 1 THEN [PartLot_UD].[CoilWeight_c] ELSE '0' END) END) END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Produced' THEN (CASE WHEN [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c] THEN (CASE WHEN [UD01].[CheckBox02] = 1 THEN [PartLot_UD].[CoilWeight_c] ELSE '0' END) END) END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Produced' THEN (CASE WHEN [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c] THEN [PartLot_UD].[TheoreticalWeight_c] ELSE NULL END) END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Produced' AND [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c] THEN [PartLot_UD].[Length_c] ELSE NULL END),
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Produced' AND [JobHead].[JobNum] = [PartTran].[JobNum] AND [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c] THEN [PartLot_UD].[SkidNum_c] ELSE ' ' END),
|
||||||
|
[UD01].[ShortChar04], [UD01].[Character02],
|
||||||
|
(CASE WHEN (CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END) = 'Produced' AND [PartTran].[TranQty] = [PartLot_UD].[CoilWeight_c] THEN [PartTran].[TranDate] ELSE NULL END),
|
||||||
|
(CASE WHEN [PartTran].[TranType] = 'MFG-STK' THEN 'Produced' ELSE (CASE WHEN [PartTran].[TranType] = 'STK-MTL' THEN 'Used' ELSE ' ' END) END),
|
||||||
|
[OrderDtl].[POLine], [OrderDtl].[Reference], [Customer].[CustID],
|
||||||
|
[PartTran].[TranType], [JobHead].[Plant], [SubQuery2].[JobHead1_Plant], [PartTran].[Plant],
|
||||||
|
[SubQuery2].[JobHead1_JobNum], [PartTran].[JobNum], [UD01].[ShortChar01],
|
||||||
|
[SubQuery2].[JobHead1_PartNum], [PartPlant].[PartNum], [PartTran].[PartNum],
|
||||||
|
[SubQuery2].[JobHead1_PartDescription], [PartTran].[PartDescription],
|
||||||
|
[PartTran].[LotNum], [PartLot].[LotNum], [PartTran].[TranQty],
|
||||||
|
[PartLot_UD].[Length_c], [PartLot_UD].[TheoreticalWeight_c], [PartLot_UD].[SkidNum_c],
|
||||||
|
[PartLot_UD].[MasterLotNum_c], [PartTran].[TranDate], [PartTran].[WareHouseCode],
|
||||||
|
[SubQuery2].[OrderHed1_PONum], [SubQuery2].[JobProd1_OrderNum],
|
||||||
|
[SubQuery2].[JobProd1_OrderLine], [SubQuery2].[JobProd1_OrderRelNum],
|
||||||
|
(CASE WHEN [JobProd].[TargetJobNum] > 0 THEN [JobProd].[TargetJobNum] ELSE [JobHead].[JobNum] END),
|
||||||
|
[JobProd].[TargetJobNum], [PartTran].[UM], [PartPlant_UD].[RcvType_c], PartLot.MfgLot
|
||||||
|
ORDER BY [JobHead_JobNum], [PartTran_LotNum] ASC, [Calculated_StartWt] DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rawRows = await execQuery<Record<string, unknown>[]>(sql, {
|
||||||
|
JobNum: jobNum,
|
||||||
|
JobNum2: jobNum,
|
||||||
|
Customer: queryCustId,
|
||||||
|
Customer2: queryCustId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return rawRows.map((r) => mapCoilByCoilRow(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map raw SQL result to CoilByCoilRow
|
||||||
|
*/
|
||||||
|
function mapCoilByCoilRow(raw: Record<string, unknown>): CoilByCoilRow {
|
||||||
|
const str = (val: unknown) => (val != null && String(val).trim() !== '') ? String(val).trim() : null;
|
||||||
|
const num = (val: unknown) => val != null ? Number(val) : 0;
|
||||||
|
const numOrNull = (val: unknown) => val != null ? Number(val) : null;
|
||||||
|
const date = (val: unknown) => val ? new Date(val as string).toISOString() : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
mfg_lot: str(raw.MfgLot),
|
||||||
|
company_name: String(raw.Company_Name ?? ''),
|
||||||
|
plant_name: String(raw.Calculated_PlantName ?? ''),
|
||||||
|
job_num: String(raw.JobHead_JobNum ?? ''),
|
||||||
|
customer_name: String(raw.Customer_Name ?? ''),
|
||||||
|
ship_to_name: str(raw.ShipTo_Name),
|
||||||
|
cust_po: str(raw.Calculated_CustPo),
|
||||||
|
cust_part: str(raw.Calculated_CustPart),
|
||||||
|
part_num: String(raw.JobHead_PartNum ?? ''),
|
||||||
|
part_description: String(raw.JobHead_PartDescription ?? ''),
|
||||||
|
prod_type: str(raw.Calculated_ProdType),
|
||||||
|
order_num: raw.JobProd_OrderNum != null ? Number(raw.JobProd_OrderNum) : null,
|
||||||
|
order_line: raw.JobProd_OrderLine != null ? Number(raw.JobProd_OrderLine) : null,
|
||||||
|
completion_date: date(raw.JobHead_JobCompletionDate),
|
||||||
|
tran_type: String(raw.Calculated_ProdOrUsed ?? '').trim(),
|
||||||
|
part_num_used: str(raw.Calculated_PartNumUsed),
|
||||||
|
part_desc_used: str(raw.Calculated_PartDescUsed),
|
||||||
|
lot_used: str(raw.Calculated_LotUsed),
|
||||||
|
start_wt: num(raw.Calculated_StartWt),
|
||||||
|
rts_wt_good: num(raw.Calculated_RtsWtGood),
|
||||||
|
rts_reject: num(raw.Calculated_RtsReject),
|
||||||
|
total_wt_used: num(raw.Calculated_TotalWtUsed),
|
||||||
|
tran_reference: str(raw.PartTran_TranReference),
|
||||||
|
prod_lot: str(raw.Calculated_ProdLot),
|
||||||
|
prod_wt_good: numOrNull(raw.Calculated_ProdGoodWt),
|
||||||
|
prod_wt_hold: numOrNull(raw.Calculated_HoldWt),
|
||||||
|
prod_wt_reject: numOrNull(raw.Calculated_RejWt),
|
||||||
|
prod_theoretical_wt: numOrNull(raw.Calculated_TheoWt),
|
||||||
|
prod_length: numOrNull(raw.Calculated_LinealFt),
|
||||||
|
prod_skid: str(raw.Calculated_SkidNum),
|
||||||
|
prod_date: date(raw.Calculated_ProdDate),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
319
src/services/jobs.ts
Normal file
319
src/services/jobs.ts
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
import { execQuery, execStoredProc, getPortalDbName } from '@/lib/epicor';
|
||||||
|
import type {
|
||||||
|
JobStatusRow,
|
||||||
|
JobTravelerData,
|
||||||
|
JobTravelerHeader,
|
||||||
|
JobTravelerShipLine,
|
||||||
|
JobTravelerMaterial,
|
||||||
|
JobTravelerOperation,
|
||||||
|
JobTravelerPaintData,
|
||||||
|
} from '@/types/jobs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get job status for a customer across all plants
|
||||||
|
* Uses OpenOrdersQueryV3 stored procedure
|
||||||
|
*/
|
||||||
|
export async function getJobStatusByPlant(
|
||||||
|
custId: string
|
||||||
|
): Promise<JobStatusRow[]> {
|
||||||
|
// HDC → HDM customer mapping
|
||||||
|
const mappedCustId = custId === 'HDC' ? 'HDM' : custId;
|
||||||
|
const dbName = getPortalDbName();
|
||||||
|
|
||||||
|
type StoredProcResult = {
|
||||||
|
Plant_Name: string;
|
||||||
|
Customer_Name: string;
|
||||||
|
JobHead_JobNum: string;
|
||||||
|
JobHead_JobReleased: boolean;
|
||||||
|
JobHead_DueDate: string | null;
|
||||||
|
CustomerPartNumber: string | null;
|
||||||
|
JobHead_PartNum: string;
|
||||||
|
JobHead_PartDescription: string;
|
||||||
|
JobHead_ProdQty: number;
|
||||||
|
JobOper_QtyCompleted: number;
|
||||||
|
JobProd_OrderNum: number | null;
|
||||||
|
JobProd_OrderLine: number | null;
|
||||||
|
JobProd_OrderRelNum: number | null;
|
||||||
|
OrderHed_PONum: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await execStoredProc<StoredProcResult[]>(
|
||||||
|
'OpenOrdersQueryV3',
|
||||||
|
{
|
||||||
|
CUSTID: mappedCustId,
|
||||||
|
DBNAME: dbName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.map((row) => ({
|
||||||
|
plant_name: row.Plant_Name,
|
||||||
|
customer_name: row.Customer_Name,
|
||||||
|
job_num: row.JobHead_JobNum,
|
||||||
|
job_released: row.JobHead_JobReleased,
|
||||||
|
due_date: row.JobHead_DueDate,
|
||||||
|
customer_part_num: row.CustomerPartNumber,
|
||||||
|
part_num: row.JobHead_PartNum,
|
||||||
|
part_description: row.JobHead_PartDescription,
|
||||||
|
prod_qty: row.JobHead_ProdQty,
|
||||||
|
qty_completed: row.JobOper_QtyCompleted,
|
||||||
|
order_num: row.JobProd_OrderNum,
|
||||||
|
order_line: row.JobProd_OrderLine,
|
||||||
|
order_rel_num: row.JobProd_OrderRelNum,
|
||||||
|
po_num: row.OrderHed_PONum,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get job traveler details for a specific job
|
||||||
|
* Returns structured data with header, shipping schedule, materials, operations, and paint data
|
||||||
|
*/
|
||||||
|
export async function getJobTraveler(
|
||||||
|
jobNum: string
|
||||||
|
): Promise<JobTravelerData | null> {
|
||||||
|
const sql = `
|
||||||
|
select
|
||||||
|
[JobHead].[Plant] as [Plant],
|
||||||
|
[JobHead].[JobNum] as [JobNum],
|
||||||
|
[Customer].[Name] as [CustomerName],
|
||||||
|
[OrderHed].[PONum] as [PONum],
|
||||||
|
[JobHead].[PartNum] as [PartNum],
|
||||||
|
[JobHead].[PartDescription] as [PartDescription],
|
||||||
|
[JobHead].[RevisionNum] as [RevisionNum],
|
||||||
|
[JobHead].[XRefPartNum] as [CustPart],
|
||||||
|
[JobHead].[ProdQty] as [JobHead_ProdQty],
|
||||||
|
[JobHead].[IUM] as [JobHead_IUM],
|
||||||
|
[JobHead].[StartDate] as [StartDate],
|
||||||
|
[JobHead].[DueDate] as [DueDate],
|
||||||
|
[JobHead].[ReqDueDate] as [ReqDueDate],
|
||||||
|
[OrderDtl].[NeedByDate] as [NeedByDate],
|
||||||
|
OrderDtl.LineStatus,
|
||||||
|
[JobProd].[OrderNum] as [SO],
|
||||||
|
[JobProd].[OrderLine] as [OrderLine],
|
||||||
|
[JobProd].[OrderRelNum] as [OrderRelNum],
|
||||||
|
[OrderDtl].[SellingQuantity] as [OrderQty],
|
||||||
|
[OrderDtl].[SalesUM] as [SalesUM],
|
||||||
|
[JobProd].[ProdQty] as [QtyFromJob],
|
||||||
|
('0.00') as [Calculated_QtyFromStk],
|
||||||
|
[ShipVia].[Description] as [ShipVia],
|
||||||
|
[ShipTo].[Name] as [ShipToName],
|
||||||
|
[JobHead].[CommentText] as [JobHead_CommentText],
|
||||||
|
[JobMtl].[MtlSeq] as MtlSeq,
|
||||||
|
[JobMtl].[PartNum] as [JobMtlPart],
|
||||||
|
[JobMtl].[Description] as [JobMtlDescription],
|
||||||
|
[JobMtl].[RequiredQty] as [JobMtlRequiredQty],
|
||||||
|
[JobMtl].[IUM] as [JobMtl_IUM],
|
||||||
|
[JobMtl].[WarehouseCode] as [JobMtlWhse],
|
||||||
|
[JobMtl].[RelatedOperation] as [JobMtlRelOp],
|
||||||
|
((case when JobMtl.IssuedComplete = 1 then 'Issued Complete' else
|
||||||
|
(case when JobMtl.IssuedComplete = 0 and JobMtl.IssuedQty = '0' then 'Open' else
|
||||||
|
(case when JobMtl.IssuedQty > 0 then 'Issued Partial' end)end)end)) as [Calculated_MtlIssueStatus],
|
||||||
|
[JobOper].[OprSeq] as [JobOperSeq],
|
||||||
|
[JobOper].[OpCode] as [JobOperCode],
|
||||||
|
[JobOper].[OpDesc] as [JobOperDescription],
|
||||||
|
[JobOper].[RunQty] as [JobOperQty],
|
||||||
|
[JobOper].[ProdCrewSize] as [JobOperRes],
|
||||||
|
[JobOper].[EstSetHoursPerMch] as [JobOperSetUpHours],
|
||||||
|
[JobOper].[EstProdHours] as [JobOperProdHours],
|
||||||
|
[JobOper].[ProdStandard] as [JobOperStandard],
|
||||||
|
[JobOper].[StartDate] as [JobOperStart],
|
||||||
|
[JobOper].[DueDate] as [JobOperDue],
|
||||||
|
((case when JobOper.JobComplete = 'true' then 'CMPL' else 'OPEN' end)) as [Calculated_JobComplete],
|
||||||
|
(SELECT TOP 1 Erp.OrderRel.WarehouseCode FROM Erp.OrderRel WHERE Erp.OrderRel.OrderNum = OrderHed.OrderNum) AS Warehouse,
|
||||||
|
Part_UD.A6A_c, Part_UD.A6B_c, Part_UD.BackcoatFilmThick_c, Part_UD.BackcoatFilmTol_c,
|
||||||
|
Part_UD.BackcoatPaintCode_c, Part_UD.BackcoatPaintDesc_c, Part_UD.BandCoil_c, Part_UD.BandSkid_c,
|
||||||
|
Part_UD.BaseMetalTV_c, Part_UD.CardboardBottom_c, Part_UD.CardboardTop_c, Part_UD.CoilID_c,
|
||||||
|
Part_UD.CoilOD_c, Part_UD.CoilsperSkid_c, Part_UD.CoilWeight_c, Part_UD.Cores_c, Part_UD.Cut_c,
|
||||||
|
Part_UD.DateCodeReqd_c, Part_UD.DFT_c, Part_UD.Draw_c, Part_UD.Drop_c, Part_UD.EndUsage_c,
|
||||||
|
Part_UD.Hardness_c, Part_UD.LineSpeed_c, Part_UD.MaxSkidWeight_c, Part_UD.MetalThickness_c,
|
||||||
|
Part_UD.MetalWidth_c, Part_UD.MetalWidthTolerance_c, Part_UD.NA_c, Part_UD.NCCA_c,
|
||||||
|
Part_UD.PcntSolidbyVolume_c, Part_UD.PcntSolidbyWeight_c, Part_UD.PcntSolventbyVolume_c,
|
||||||
|
Part_UD.PcntSolventbyWeight_c, Part_UD.PcntWaterbyVolume_c, Part_UD.PcntWaterbyWeight_c,
|
||||||
|
Part_UD.PlasticWrap_c, Part_UD.PrimBackFilmDesc_c, Part_UD.PrimBackFilmTol_c,
|
||||||
|
Part_UD.PrimBackPaintCode_c, Part_UD.PrimBackPaintDesc_c, Part_UD.PrimTopFilmThick_c,
|
||||||
|
Part_UD.PrimTopFilmTol_c, Part_UD.PrimTopPaintCode_c, Part_UD.PrimTopPaIntDesc_c,
|
||||||
|
Part_UD.RawDescription_c, Part_UD.RawPartNum_c, Part_UD.SkidType_c,
|
||||||
|
Part_UD.Slit1Cuts_c, Part_UD.Slit1Tol_c, Part_UD.Slit1Width_c,
|
||||||
|
Part_UD.Slit2Cuts_c, Part_UD.Slit2Tol_c, Part_UD.Slit2Width_c,
|
||||||
|
Part_UD.Slit3Cuts_c, Part_UD.Slit3Tol_c, Part_UD.Slit3Width_c,
|
||||||
|
Part_UD.Slit4Cuts_c, Part_UD.Slit4Tol_c, Part_UD.Slit4Width_c,
|
||||||
|
Part_UD.Spacers_c, Part_UD.SqFtperLB_c, Part_UD.TBend_c, Part_UD.TechComments_c,
|
||||||
|
Part_UD.ThicknessTolerance_c, Part_UD.TopcoatFilmThick_c, Part_UD.TopcoatFilmTol_c,
|
||||||
|
Part_UD.TopcoatPaintCode_c, Part_UD.TopcoatPaintDesc_c, Part_UD.VOCGal_c,
|
||||||
|
Part_UD.WeightperGallon_c, Part_UD.CoilWeightC_c, Part_UD.CoilODC_c
|
||||||
|
from Erp.JobHead as JobHead
|
||||||
|
left join Erp.JobProd as JobProd on JobHead.Company = JobProd.Company And JobHead.JobNum = JobProd.JobNum
|
||||||
|
left join Erp.OrderDtl as OrderDtl on JobProd.Company = OrderDtl.Company And JobProd.OrderNum = OrderDtl.OrderNum And JobProd.OrderLine = OrderDtl.OrderLine
|
||||||
|
left join Erp.OrderHed as OrderHed on OrderDtl.Company = OrderHed.Company And OrderDtl.OrderNum = OrderHed.OrderNum
|
||||||
|
left join Erp.ShipVia as ShipVia on OrderHed.Company = ShipVia.Company And OrderHed.ShipViaCode = ShipVia.ShipViaCode
|
||||||
|
left join Erp.Customer as Customer on OrderHed.Company = Customer.Company And OrderHed.BTCustNum = Customer.CustNum
|
||||||
|
left outer join Erp.JobMtl as JobMtl on JobHead.Company = JobMtl.Company And JobHead.JobNum = JobMtl.JobNum
|
||||||
|
left join Erp.JobOper as JobOper on JobHead.Company = JobOper.Company And JobHead.JobNum = JobOper.JobNum
|
||||||
|
left join Erp.ShipTo as ShipTo on OrderHed.CustNum = ShipTo.CustNum And OrderHed.ShipToNum = ShipTo.ShipToNum
|
||||||
|
JOIN Erp.Part ON Part.PartNum = JobHead.PartNum
|
||||||
|
JOIN Erp.Part_UD ON Part_UD.ForeignSysRowID = Part.SysRowID
|
||||||
|
where (JobHead.JobNum = @JobNum or JobHead.JobNum = @JobNum1)
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
type QueryResult = {
|
||||||
|
Plant: string;
|
||||||
|
JobNum: string;
|
||||||
|
CustomerName: string;
|
||||||
|
PONum: string | null;
|
||||||
|
PartNum: string;
|
||||||
|
PartDescription: string;
|
||||||
|
RevisionNum: string;
|
||||||
|
CustPart: string | null;
|
||||||
|
JobHead_ProdQty: number;
|
||||||
|
JobHead_IUM: string;
|
||||||
|
StartDate: string | null;
|
||||||
|
DueDate: string | null;
|
||||||
|
ReqDueDate: string | null;
|
||||||
|
NeedByDate: string | null;
|
||||||
|
LineStatus: string | null;
|
||||||
|
SO: number | null;
|
||||||
|
OrderLine: number | null;
|
||||||
|
OrderRelNum: number | null;
|
||||||
|
OrderQty: number | null;
|
||||||
|
SalesUM: string | null;
|
||||||
|
QtyFromJob: number | null;
|
||||||
|
Calculated_QtyFromStk: string;
|
||||||
|
ShipVia: string | null;
|
||||||
|
ShipToName: string | null;
|
||||||
|
JobHead_CommentText: string | null;
|
||||||
|
MtlSeq: number | null;
|
||||||
|
JobMtlPart: string | null;
|
||||||
|
JobMtlDescription: string | null;
|
||||||
|
JobMtlRequiredQty: number | null;
|
||||||
|
JobMtl_IUM: string | null;
|
||||||
|
JobMtlWhse: string | null;
|
||||||
|
JobMtlRelOp: number | null;
|
||||||
|
Calculated_MtlIssueStatus: string | null;
|
||||||
|
JobOperSeq: number | null;
|
||||||
|
JobOperCode: string | null;
|
||||||
|
JobOperDescription: string | null;
|
||||||
|
JobOperQty: number | null;
|
||||||
|
JobOperRes: number | null;
|
||||||
|
JobOperSetUpHours: number | null;
|
||||||
|
JobOperProdHours: number | null;
|
||||||
|
JobOperStandard: number | null;
|
||||||
|
JobOperStart: string | null;
|
||||||
|
JobOperDue: string | null;
|
||||||
|
Calculated_JobComplete: string | null;
|
||||||
|
Warehouse: string | null;
|
||||||
|
[key: string]: unknown; // For Part_UD fields ending in _c
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await execQuery<QueryResult[]>(sql, {
|
||||||
|
JobNum: jobNum,
|
||||||
|
JobNum1: jobNum,
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstRow = results[0];
|
||||||
|
if (!firstRow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract header from first row
|
||||||
|
const header: JobTravelerHeader = {
|
||||||
|
plant: firstRow.Plant,
|
||||||
|
job_num: firstRow.JobNum,
|
||||||
|
customer_name: firstRow.CustomerName,
|
||||||
|
po_num: firstRow.PONum,
|
||||||
|
part_num: firstRow.PartNum,
|
||||||
|
part_description: firstRow.PartDescription,
|
||||||
|
revision: firstRow.RevisionNum,
|
||||||
|
cust_part: firstRow.CustPart,
|
||||||
|
prod_qty: firstRow.JobHead_ProdQty,
|
||||||
|
ium: firstRow.JobHead_IUM,
|
||||||
|
start_date: firstRow.StartDate ? new Date(firstRow.StartDate).toISOString() : null,
|
||||||
|
due_date: firstRow.DueDate ? new Date(firstRow.DueDate).toISOString() : null,
|
||||||
|
req_due_date: firstRow.ReqDueDate ? new Date(firstRow.ReqDueDate).toISOString() : null,
|
||||||
|
comment_text: firstRow.JobHead_CommentText,
|
||||||
|
warehouse: firstRow.Warehouse,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deduplicate shipping schedule by OrderNum+OrderLine+OrderRelNum
|
||||||
|
const shippingSet = new Set<string>();
|
||||||
|
const shipping_schedule: JobTravelerShipLine[] = [];
|
||||||
|
|
||||||
|
for (const row of results) {
|
||||||
|
if (row.SO != null && row.OrderLine != null && row.OrderRelNum != null) {
|
||||||
|
const key = `${row.SO}-${row.OrderLine}-${row.OrderRelNum}`;
|
||||||
|
if (!shippingSet.has(key)) {
|
||||||
|
shippingSet.add(key);
|
||||||
|
shipping_schedule.push({
|
||||||
|
need_by_date: row.NeedByDate ? new Date(row.NeedByDate).toISOString() : null,
|
||||||
|
line_status: row.LineStatus,
|
||||||
|
order_num: row.SO,
|
||||||
|
order_line: row.OrderLine,
|
||||||
|
order_rel_num: row.OrderRelNum,
|
||||||
|
order_qty: row.OrderQty,
|
||||||
|
sales_um: row.SalesUM,
|
||||||
|
qty_from_job: row.QtyFromJob,
|
||||||
|
qty_from_stock: row.Calculated_QtyFromStk,
|
||||||
|
ship_via: row.ShipVia,
|
||||||
|
ship_to: row.ShipToName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate materials by MtlSeq
|
||||||
|
const materialsSet = new Set<number>();
|
||||||
|
const materials: JobTravelerMaterial[] = [];
|
||||||
|
|
||||||
|
for (const row of results) {
|
||||||
|
if (row.MtlSeq != null && !materialsSet.has(row.MtlSeq)) {
|
||||||
|
materialsSet.add(row.MtlSeq);
|
||||||
|
materials.push({
|
||||||
|
mtl_seq: row.MtlSeq,
|
||||||
|
part_num: row.JobMtlPart,
|
||||||
|
description: row.JobMtlDescription,
|
||||||
|
required_qty: row.JobMtlRequiredQty,
|
||||||
|
ium: row.JobMtl_IUM,
|
||||||
|
warehouse_code: row.JobMtlWhse,
|
||||||
|
related_operation: row.JobMtlRelOp,
|
||||||
|
issue_status: row.Calculated_MtlIssueStatus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate operations by JobOperSeq
|
||||||
|
const operationsSet = new Set<number>();
|
||||||
|
const operations: JobTravelerOperation[] = [];
|
||||||
|
|
||||||
|
for (const row of results) {
|
||||||
|
if (row.JobOperSeq != null && !operationsSet.has(row.JobOperSeq)) {
|
||||||
|
operationsSet.add(row.JobOperSeq);
|
||||||
|
operations.push({
|
||||||
|
opr_seq: row.JobOperSeq,
|
||||||
|
op_code: row.JobOperCode,
|
||||||
|
op_desc: row.JobOperDescription,
|
||||||
|
run_qty: row.JobOperQty,
|
||||||
|
crew_size: row.JobOperRes,
|
||||||
|
est_setup_hours: row.JobOperSetUpHours,
|
||||||
|
est_prod_hours: row.JobOperProdHours,
|
||||||
|
prod_standard: row.JobOperStandard,
|
||||||
|
start_date: row.JobOperStart ? new Date(row.JobOperStart).toISOString() : null,
|
||||||
|
due_date: row.JobOperDue ? new Date(row.JobOperDue).toISOString() : null,
|
||||||
|
status: row.Calculated_JobComplete,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract paint data (all Part_UD fields ending with _c)
|
||||||
|
const paint_data: JobTravelerPaintData = {};
|
||||||
|
for (const key of Object.keys(firstRow)) {
|
||||||
|
if (key.endsWith('_c')) {
|
||||||
|
paint_data[key] = firstRow[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
header,
|
||||||
|
shipping_schedule,
|
||||||
|
materials,
|
||||||
|
operations,
|
||||||
|
paint_data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -31,3 +31,42 @@ export type CoilReceiptRow = {
|
||||||
supplier_name: string | null; // PartLot.PartLotDescription or view SupplierName
|
supplier_name: string | null; // PartLot.PartLotDescription or view SupplierName
|
||||||
mill_order_num: string | null; // PartLot.Batch or view MillOrderNum
|
mill_order_num: string | null; // PartLot.Batch or view MillOrderNum
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// C-010: Coil-by-Coil Report
|
||||||
|
export type CoilByCoilRow = {
|
||||||
|
// Header fields
|
||||||
|
mfg_lot: string | null;
|
||||||
|
company_name: string;
|
||||||
|
plant_name: string;
|
||||||
|
job_num: string;
|
||||||
|
customer_name: string;
|
||||||
|
ship_to_name: string | null;
|
||||||
|
cust_po: string | null;
|
||||||
|
cust_part: string | null;
|
||||||
|
part_num: string;
|
||||||
|
part_description: string;
|
||||||
|
prod_type: string | null;
|
||||||
|
order_num: number | null;
|
||||||
|
order_line: number | null;
|
||||||
|
completion_date: string | null;
|
||||||
|
// Transaction type
|
||||||
|
tran_type: string; // 'Used' or 'Produced'
|
||||||
|
// Used fields
|
||||||
|
part_num_used: string | null;
|
||||||
|
part_desc_used: string | null;
|
||||||
|
lot_used: string | null;
|
||||||
|
start_wt: number;
|
||||||
|
rts_wt_good: number;
|
||||||
|
rts_reject: number;
|
||||||
|
total_wt_used: number;
|
||||||
|
tran_reference: string | null;
|
||||||
|
// Produced fields
|
||||||
|
prod_lot: string | null;
|
||||||
|
prod_wt_good: number | null;
|
||||||
|
prod_wt_hold: number | null;
|
||||||
|
prod_wt_reject: number | null;
|
||||||
|
prod_theoretical_wt: number | null;
|
||||||
|
prod_length: number | null;
|
||||||
|
prod_skid: string | null;
|
||||||
|
prod_date: string | null;
|
||||||
|
};
|
||||||
|
|
|
||||||
93
src/types/jobs.ts
Normal file
93
src/types/jobs.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
/**
|
||||||
|
* Job Type Definitions
|
||||||
|
*
|
||||||
|
* Types for Job Status by Plant (C-011) and Job Traveler (C-012) reports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// C-011: Job Status by Plant (from OpenOrdersQueryV3 stored proc)
|
||||||
|
export type JobStatusRow = {
|
||||||
|
plant_name: string;
|
||||||
|
customer_name: string;
|
||||||
|
job_num: string;
|
||||||
|
job_released: boolean;
|
||||||
|
due_date: string | null;
|
||||||
|
customer_part_num: string | null;
|
||||||
|
part_num: string;
|
||||||
|
part_description: string;
|
||||||
|
prod_qty: number;
|
||||||
|
qty_completed: number;
|
||||||
|
order_num: number | null;
|
||||||
|
order_line: number | null;
|
||||||
|
order_rel_num: number | null;
|
||||||
|
po_num: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// C-012: Job Traveler
|
||||||
|
export type JobTravelerHeader = {
|
||||||
|
plant: string;
|
||||||
|
job_num: string;
|
||||||
|
customer_name: string;
|
||||||
|
po_num: string | null;
|
||||||
|
part_num: string;
|
||||||
|
part_description: string;
|
||||||
|
revision: string | null;
|
||||||
|
cust_part: string | null;
|
||||||
|
prod_qty: number;
|
||||||
|
ium: string;
|
||||||
|
start_date: string | null;
|
||||||
|
due_date: string | null;
|
||||||
|
req_due_date: string | null;
|
||||||
|
comment_text: string | null;
|
||||||
|
warehouse: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobTravelerShipLine = {
|
||||||
|
need_by_date: string | null;
|
||||||
|
line_status: string | null;
|
||||||
|
order_num: number | null;
|
||||||
|
order_line: number | null;
|
||||||
|
order_rel_num: number | null;
|
||||||
|
order_qty: number | null;
|
||||||
|
sales_um: string | null;
|
||||||
|
qty_from_job: number | null;
|
||||||
|
qty_from_stock: string;
|
||||||
|
ship_via: string | null;
|
||||||
|
ship_to: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobTravelerMaterial = {
|
||||||
|
mtl_seq: number;
|
||||||
|
part_num: string | null;
|
||||||
|
description: string | null;
|
||||||
|
required_qty: number | null;
|
||||||
|
ium: string | null;
|
||||||
|
warehouse_code: string | null;
|
||||||
|
related_operation: number | null;
|
||||||
|
issue_status: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobTravelerOperation = {
|
||||||
|
opr_seq: number;
|
||||||
|
op_code: string | null;
|
||||||
|
op_desc: string | null;
|
||||||
|
run_qty: number | null;
|
||||||
|
crew_size: number | null;
|
||||||
|
est_setup_hours: number | null;
|
||||||
|
est_prod_hours: number | null;
|
||||||
|
prod_standard: number | null;
|
||||||
|
start_date: string | null;
|
||||||
|
due_date: string | null;
|
||||||
|
status: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobTravelerPaintData = {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobTravelerData = {
|
||||||
|
header: JobTravelerHeader;
|
||||||
|
shipping_schedule: JobTravelerShipLine[];
|
||||||
|
materials: JobTravelerMaterial[];
|
||||||
|
operations: JobTravelerOperation[];
|
||||||
|
paint_data: JobTravelerPaintData | null;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue