Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ jobs:
with:
node-version: 20
cache: "npm"
- run: npm ci
- name: Install dependencies
run: |
npm config set registry https://registry.npmjs.org/
npm ci --prefer-offline --no-audib
- run: npm run lint:ci

typecheck:
Expand All @@ -33,7 +36,10 @@ jobs:
with:
node-version: 20
cache: "npm"
- run: npm ci
- name: Install dependencies
run: |
npm config set registry https://registry.npmjs.org/
npm ci --prefer-offline --no-audit
- run: npm run typecheck

test:
Expand All @@ -48,5 +54,8 @@ jobs:
with:
node-version: 20
cache: "npm"
- run: npm ci
- name: Install dependencies
run: |
npm config set registry https://registry.npmjs.org/
npm ci --prefer-offline --no-audit
- run: npm run test:ci
56 changes: 56 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"@next/font": "^14.2.15",
"@supabase/supabase-js": "^2.56.0",
"@tanstack/react-query-devtools": "^5.85.5",
"next": "15.5.0",
"react": "19.1.0",
"react-dom": "19.1.0",
Expand Down
212 changes: 212 additions & 0 deletions src/app/api/roster/[yearMonth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { type NextRequest, NextResponse } from 'next/server';
import { supabase } from '@/lib/supabase';
import type { ShiftTableData } from '@/types';

export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ yearMonth: string }> }
) {
try {
const { yearMonth } = await params;

// Validate yearMonth format (YYYY-MM)
if (!/^\d{4}-\d{2}$/.test(yearMonth)) {
return NextResponse.json(
{ success: false, error: 'Invalid yearMonth format. Use YYYY-MM' },
{ status: 400 }
);
}

// Get crew roster first
const { data: roster, error: rosterError } = await supabase
.from('crew_roster')
.select('*')
.eq('year_month', yearMonth)
.single();

if (rosterError) {
if (rosterError.code === 'PGRST116') {
return NextResponse.json(
{ success: false, error: 'Crew roster not found' },
{ status: 404 }
);
}
throw rosterError;
}

console.log('Roster found:', roster);

// Get all flight days first
const { data: allFlightDays, error: flightDaysError } = await supabase
.from('flight_day')
.select('*')
.eq('roster_id', roster.id)
.order('day');

if (flightDaysError) {
throw flightDaysError;
}

console.log('All flight days found:', allFlightDays);

// Filter days that have flight pairs
const daysWithFlightPairs = await Promise.all(
allFlightDays?.map(async (day: { id: string; day: string }) => {
try {
const { data: flightPairs, error: flightPairsError } = await supabase
.from('flight_pair')
.select(`
id,
flight_code,
out_dep_airport,
out_arr_airport,
out_dep_min,
out_arr_min,
in_dep_airport,
in_arr_airport,
in_dep_min,
in_arr_min
`)
.eq('day_id', day.id);

if (flightPairsError) {
console.error(`Flight pairs error for day ${day.day}:`, flightPairsError);
return null; // Skip this day
}

// Only include days that have flight pairs
if (!flightPairs || flightPairs.length === 0) {
return null; // Skip this day
}

// Get assignments for each flight pair
const flightPairsWithAssignments = await Promise.all(
flightPairs?.map(
async (fp: {
id: string;
flight_code: string;
out_dep_airport: string;
out_arr_airport: string;
out_dep_min: number;
out_arr_min: number;
in_dep_airport?: string;
in_arr_airport?: string;
in_dep_min?: number;
in_arr_min?: number;
}) => {
try {
const { data: assignments, error: assignmentsError } = await supabase
.from('assignment')
.select(`
id,
assignment_scope,
crew_member (
id,
employee_id,
name,
role,
base_airport,
status
)
`)
.eq('flight_pair_id', fp.id);

if (assignmentsError) {
console.error(`Assignments error for flight pair ${fp.id}:`, assignmentsError);
return {
...fp,
assignments: [],
};
}

return {
...fp,
assignments: assignments || [],
};
} catch (error) {
console.error(`Error processing flight pair ${fp.id}:`, error);
return {
...fp,
assignments: [],
};
}
}
) || []
);

return {
day: day.day,
flight_pairs: flightPairsWithAssignments,
};
} catch (error) {
console.error(`Error processing day ${day.day}:`, error);
return null; // Skip this day
}
}) || []
);

// Filter out null values (days without flight pairs)
const filteredDays = daysWithFlightPairs.filter((day) => day !== null);

console.log('Filtered days with flight pairs:', filteredDays);

// Transform data for frontend table
const transformedDays = filteredDays.map((day) => {
const date = new Date(day.day);
const dayOfWeek = ['日', '月', '火', '水', '木', '金', '土'][date.getDay()];

return {
day: day.day,
dayOfWeek,
isHoliday: false, // TODO: add holiday check logic
holidayName: undefined,
flights: day.flight_pairs.map((fp) => ({
id: parseInt(fp.id, 10),
flight_code: fp.flight_code,
out_dep_airport: fp.out_dep_airport,
out_arr_airport: fp.out_arr_airport,
out_dep_min: fp.out_dep_min,
out_arr_min: fp.out_arr_min,
in_dep_airport: fp.in_dep_airport,
in_arr_airport: fp.in_arr_airport,
in_dep_min: fp.in_dep_min,
in_arr_min: fp.in_arr_min,
crew: fp.assignments.map((assignment) => {
// crew_member is an array, take the first one
const crewMember = Array.isArray(assignment.crew_member)
? assignment.crew_member[0]
: assignment.crew_member;

return {
id: parseInt(assignment.id, 10),
employee_id: crewMember.employee_id,
name: crewMember.name,
role: crewMember.role,
assignment_scope: assignment.assignment_scope,
};
}),
})),
};
});

const response: ShiftTableData = {
roster: {
id: roster.id,
year_month: roster.year_month,
status: roster.status,
created_at: roster.created_at,
updated_at: roster.updated_at,
},
days: transformedDays,
};

return NextResponse.json({ success: true, data: response });
} catch (error) {
console.error('Error fetching crew roster:', error);
console.error('Error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
});
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 });
}
}
14 changes: 0 additions & 14 deletions src/app/approval/page.tsx

This file was deleted.

33 changes: 21 additions & 12 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import type { Metadata } from 'next';
import { notoSansJP } from '@/lib/fonts';
import './globals.css';
import { Noto_Sans_JP } from 'next/font/google';
import Header from '@/components/layout/Header';
import TabMenu from '@/components/layout/TabMenu';
import { QueryProvider } from '@/providers/QueryProvider';
import './globals.css';

const notoSansJP = Noto_Sans_JP({
subsets: ['latin'],
weight: ['400', '500', '700'],
variable: '--font-noto-sans-jp',
});

export const metadata: Metadata = {
title: 'GWAir Crew Planner',
title: 'GWAir Crew Roster',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja" className={notoSansJP.variable}>
<body className={notoSansJP.className}>
<div className="min-h-screen bg-gray-50">
<Header />
<main className="pt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<TabMenu />
{children}
</div>
</main>
</div>
<QueryProvider>
<div className="min-h-screen bg-gray-50">
<Header />
<main className="pt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<TabMenu />
{children}
</div>
</main>
</div>
</QueryProvider>
</body>
</html>
);
Expand Down
1 change: 0 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { redirect } from 'next/navigation';

export default function HomePage() {
// 기본적으로 시프트 안 검토 페이지로 리다이렉트
redirect('/review');
}
Loading