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)
|
||||
|
||||
**Progress:** 9/16 tasks complete
|
||||
**Progress:** 12/16 tasks complete
|
||||
|
||||
### C-001: Dashboard
|
||||
- [x] Dashboard page at `/(portal)/dashboard/page.tsx`
|
||||
|
|
@ -230,25 +230,33 @@
|
|||
- **Deps:** F-006, F-009 | **Est:** 4 hrs | **Status:** ✅ Complete
|
||||
|
||||
### C-010: Coil-by-Coil Report
|
||||
- [ ] `/(portal)/coil-activity/coil-by-coil/page.tsx`
|
||||
- [ ] Job number search input
|
||||
- [ ] Service: `getCoilByCoil(jobNum, customer)` using `CoilByCoil.sql`
|
||||
- [ ] Detail display
|
||||
- **Deps:** F-006 | **Est:** 3 hrs
|
||||
- [x] `/(portal)/coil-activity/coil-by-coil/page.tsx` — job number search → coil detail display
|
||||
- [x] Job number search input with search button
|
||||
- [x] Service: `getCoilByCoil(jobNum, customer)` using full 782-line `CoilByCoil.sql` embedded in service
|
||||
- [x] Header info card (Job#, Customer, PO, Part, Plant, etc.)
|
||||
- [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
|
||||
- [ ] `/(portal)/jobs/status/page.tsx`
|
||||
- [ ] Service: `getJobStatusByPlantByCustomer(custId, dbName)` using `JobStatusByPlantByCustomer.sql`
|
||||
- [ ] Note: requires DBNAME cross-database parameter
|
||||
- [ ] Data table grouped by plant
|
||||
- **Deps:** F-006 | **Est:** 3 hrs
|
||||
- [x] `/(portal)/jobs/status/page.tsx` — auto-loads on mount
|
||||
- [x] Service: `getJobStatusByPlant(custId)` using `OpenOrdersQueryV3` stored procedure
|
||||
- [x] DBNAME cross-database parameter via `getPortalDbName()`
|
||||
- [x] Data table grouped by plant with plant section header rows
|
||||
- [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
|
||||
- [ ] `/(portal)/jobs/[jobNum]/traveler/page.tsx`
|
||||
- [ ] Service: `getJobTraveler(jobNum)` using `jobTraveler.sql`
|
||||
- [ ] Detailed routing/operations view
|
||||
- [ ] PDF export
|
||||
- **Deps:** F-006 | **Est:** 4 hrs
|
||||
- [x] `/(portal)/jobs/traveler/page.tsx` — job number search page
|
||||
- [x] `/(portal)/jobs/[jobNum]/traveler/page.tsx` — multi-section detail page
|
||||
- [x] Service: `getJobTraveler(jobNum)` using full `jobTraveler.sql` with Part_UD paint data
|
||||
- [x] Structured data: header, shipping schedule, raw materials, operations, paint specifications (deduplicated from flat SQL results)
|
||||
- [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
|
||||
- [ ] `/(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',
|
||||
icon: <Briefcase className="h-5 w-5" />,
|
||||
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 type { CoilUsageRow, CoilReceiptRow } from '@/types/coil-activity';
|
||||
import type { CoilUsageRow, CoilReceiptRow, CoilByCoilRow } from '@/types/coil-activity';
|
||||
|
||||
// =============================================================================
|
||||
// Coil Usage
|
||||
|
|
@ -256,3 +256,306 @@ function mapCoilReceiptRow(raw: Record<string, unknown>): CoilReceiptRow {
|
|||
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
|
||||
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