-
Notifications
You must be signed in to change notification settings - Fork 0
feat: schedule ingestion engine #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chiptus
wants to merge
23
commits into
main
Choose a base branch
from
feat/schedule-ingestion
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
434667d
feat(edge-fn): add diff-schedule Edge Function with unit tests
chiptus 7a95de2
feat(edge-fn): add commit-schedule Edge Function and RPC migration
chiptus 657e3bb
feat(frontend): add schedule import wizard UI
chiptus 5d48245
refactor(frontend): split ScheduleImportWizard into focused components
chiptus 32a626b
fix(test): exclude Deno and Playwright files from Vitest
chiptus c8110dd
refactor: remove old client-side CSV import
chiptus 113d396
fix(rpc): scope set_artists writes to verified update and use ROW_COUNT
claude 2125cab
fix(rpc): add ON CONFLICT DO NOTHING to create-set artist insert
claude 1a9c305
fix(import): invalidate queries via key factories
claude 1cfb1ce
chore(import): remove orphaned CSV import code and import CsvRow type
claude 1d6e869
refactor(import): extract MismatchRow and use useId for stable HTML ids
claude 1965236
test(commit-schedule): clean up created sets and select id for delete
claude af733df
refactor(import): drive async actions through useMutation
claude c977e79
refactor(import): split CsvUploadStep into TimezonePicker and CsvDrop…
claude 90339e4
refactor(import): extract OrphanedItem row component
claude dd2df7a
refactor(import): co-locate FestivalScheduleImport in its route file
claude 6b2b47f
refactor(import): load edition via route loader instead of useQuery
claude a0c36cb
refactor(diff): split computeDiff into per-row helpers
claude 33f9d67
feat(commit-schedule): validate request body with zod
claude 9f7fc1c
refactor(rpc): extract commit_schedule helpers for stage lookup, slug…
claude 85c28b7
fix(test): exclude supabase tests in vitest config and stub supabase env
claude 8a2d465
fix(lint): convert arrow function and drop unused destructured data
chiptus f9637b1
fix(rpc): make commit_schedule migration idempotent and dedup stages
chiptus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { CheckCircle2, RotateCcw } from "lucide-react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Card, CardContent } from "@/components/ui/card"; | ||
| import { type CommitResult } from "@/services/scheduleImportService"; | ||
|
|
||
| type Props = { | ||
| result: CommitResult; | ||
| onReset: () => void; | ||
| }; | ||
|
|
||
| export function CommitResultCard({ result, onReset }: Props) { | ||
| return ( | ||
| <Card> | ||
| <CardContent className="pt-6 space-y-4"> | ||
| <div className="flex items-center gap-3 text-green-600"> | ||
| <CheckCircle2 className="h-6 w-6" /> | ||
| <span className="font-medium">Schedule imported successfully</span> | ||
| </div> | ||
| <ul className="text-sm text-muted-foreground space-y-1"> | ||
| <li>{result.setsCreated} set{result.setsCreated !== 1 ? "s" : ""} created</li> | ||
| <li>{result.setsUpdated} set{result.setsUpdated !== 1 ? "s" : ""} updated</li> | ||
| {result.setsArchived > 0 && ( | ||
| <li>{result.setsArchived} set{result.setsArchived !== 1 ? "s" : ""} archived</li> | ||
| )} | ||
| </ul> | ||
| <Button variant="outline" onClick={onReset}> | ||
| <RotateCcw className="h-4 w-4 mr-2" /> | ||
| Import another file | ||
| </Button> | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { useRef } from "react"; | ||
| import { Upload } from "lucide-react"; | ||
| import { Label } from "@/components/ui/label"; | ||
|
|
||
| type Props = { | ||
| fileName: string | null; | ||
| rowCount: number; | ||
| onFileSelected: (file: File) => void; | ||
| }; | ||
|
|
||
| export function CsvDropZone({ fileName, rowCount, onFileSelected }: Props) { | ||
| const fileRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| function handleChange(e: React.ChangeEvent<HTMLInputElement>) { | ||
| const file = e.target.files?.[0]; | ||
| if (file) onFileSelected(file); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="space-y-2"> | ||
| <Label>CSV File</Label> | ||
| <div | ||
| className="border-2 border-dashed border-muted-foreground/30 rounded-lg p-8 text-center cursor-pointer hover:border-muted-foreground/60 transition-colors" | ||
| onClick={() => fileRef.current?.click()} | ||
| > | ||
| <Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" /> | ||
| {fileName ? ( | ||
| <p className="text-sm font-medium">{fileName}</p> | ||
| ) : ( | ||
| <p className="text-sm text-muted-foreground">Click to upload CSV</p> | ||
| )} | ||
| {rowCount > 0 && ( | ||
| <p className="text-xs text-muted-foreground mt-1"> | ||
| {rowCount} rows parsed | ||
| </p> | ||
| )} | ||
| </div> | ||
| <input | ||
| ref={fileRef} | ||
| type="file" | ||
| accept=".csv" | ||
| className="hidden" | ||
| onChange={handleChange} | ||
| /> | ||
| <p className="text-xs text-muted-foreground"> | ||
| Required column: <code>Artists</code> (use <code>|</code> for B2B, e.g.{" "} | ||
| <code>Carl Cox | Peggy Gou</code>). Optional: <code>Set Name</code>,{" "} | ||
| <code>Stage</code>, <code>Date</code> (YYYY-MM-DD),{" "} | ||
| <code>Start Time</code> (HH:MM), <code>End Time</code> (HH:MM),{" "} | ||
| <code>Description</code>. | ||
| </p> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { useState } from "react"; | ||
| import { Loader2 } from "lucide-react"; | ||
| import { useMutation } from "@tanstack/react-query"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { | ||
| parseScheduleCsv, | ||
| callDiffSchedule, | ||
| type CsvRow, | ||
| type DiffResult, | ||
| } from "@/services/scheduleImportService"; | ||
| import { TimezonePicker } from "./TimezonePicker"; | ||
| import { CsvDropZone } from "./CsvDropZone"; | ||
|
|
||
| type Props = { | ||
| festivalEditionId: string; | ||
| onDiffReady: (diff: DiffResult) => void; | ||
| }; | ||
|
|
||
| async function readFile(file: File): Promise<CsvRow[]> { | ||
| const content = await file.text(); | ||
| const parsed = parseScheduleCsv(content); | ||
| if (parsed.length === 0) { | ||
| throw new Error( | ||
| "No valid rows found. Make sure your CSV has an 'Artists' column.", | ||
| ); | ||
| } | ||
| return parsed; | ||
| } | ||
|
|
||
| export function CsvUploadStep({ festivalEditionId, onDiffReady }: Props) { | ||
| const [timezone, setTimezone] = useState("Europe/Lisbon"); | ||
| const [fileName, setFileName] = useState<string | null>(null); | ||
|
|
||
| const readFileMutation = useMutation({ mutationFn: readFile }); | ||
| const analyseMutation = useMutation({ | ||
| mutationFn: (rows: CsvRow[]) => | ||
| callDiffSchedule(festivalEditionId, timezone, rows), | ||
| onSuccess: onDiffReady, | ||
| }); | ||
|
|
||
| const rows = readFileMutation.data ?? []; | ||
| const error = | ||
| analyseMutation.error?.message ?? readFileMutation.error?.message ?? null; | ||
|
|
||
| function handleFileSelected(file: File) { | ||
| setFileName(file.name); | ||
| analyseMutation.reset(); | ||
| readFileMutation.mutate(file); | ||
| } | ||
|
|
||
| function handleAnalyse() { | ||
| if (rows.length === 0) return; | ||
| analyseMutation.mutate(rows); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="space-y-6"> | ||
| <TimezonePicker value={timezone} onChange={setTimezone} /> | ||
| <CsvDropZone | ||
| fileName={fileName} | ||
| rowCount={rows.length} | ||
| onFileSelected={handleFileSelected} | ||
| /> | ||
|
|
||
| {error && <p className="text-sm text-destructive">{error}</p>} | ||
|
|
||
| <Button | ||
| onClick={handleAnalyse} | ||
| disabled={rows.length === 0 || analyseMutation.isPending} | ||
| className="w-full" | ||
| > | ||
| {analyseMutation.isPending ? ( | ||
| <> | ||
| <Loader2 className="h-4 w-4 mr-2 animate-spin" /> | ||
| Analysing… | ||
| </> | ||
| ) : ( | ||
| "Analyse Schedule" | ||
| )} | ||
| </Button> | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import { AlertCircle, Loader2 } from "lucide-react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | ||
| import { | ||
| type DiffResult, | ||
| type StageMismatchResolution, | ||
| type OrphanResolution, | ||
| } from "@/services/scheduleImportService"; | ||
| import { DiffSummaryBanner } from "./DiffSummaryBanner"; | ||
| import { StageMismatchResolver } from "./StageMismatchResolver"; | ||
| import { OrphanedSetsPanel } from "./OrphanedSetsPanel"; | ||
|
|
||
| type DbStage = { id: string; name: string }; | ||
|
|
||
| type Props = { | ||
| diff: DiffResult; | ||
| dbStages: DbStage[]; | ||
| stageMismatchResolutions: Record<string, StageMismatchResolution>; | ||
| orphanResolutions: Record<string, OrphanResolution>; | ||
| onStageMismatchChange: (csvValue: string, resolution: StageMismatchResolution) => void; | ||
| onOrphanChange: (setId: string, resolution: OrphanResolution) => void; | ||
| onCommit: () => void; | ||
| onReset: () => void; | ||
| committing: boolean; | ||
| commitError: string | null; | ||
| canCommit: boolean; | ||
| }; | ||
|
|
||
| export function DiffReviewStep({ | ||
| diff, | ||
| dbStages, | ||
| stageMismatchResolutions, | ||
| orphanResolutions, | ||
| onStageMismatchChange, | ||
| onOrphanChange, | ||
| onCommit, | ||
| onReset, | ||
| committing, | ||
| commitError, | ||
| canCommit, | ||
| }: Props) { | ||
| return ( | ||
| <Card> | ||
| <CardHeader> | ||
| <CardTitle>Review Changes</CardTitle> | ||
| </CardHeader> | ||
| <CardContent className="space-y-6"> | ||
| <DiffSummaryBanner diff={diff} /> | ||
|
|
||
| <StageMismatchResolver | ||
| mismatches={diff.conflicts.stageNameMismatches} | ||
| dbStages={dbStages} | ||
| resolutions={stageMismatchResolutions} | ||
| onChange={onStageMismatchChange} | ||
| /> | ||
|
|
||
| <OrphanedSetsPanel | ||
| orphanedSets={diff.conflicts.orphanedSets} | ||
| resolutions={orphanResolutions} | ||
| onChange={onOrphanChange} | ||
| /> | ||
|
|
||
| {commitError && ( | ||
| <div className="flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive"> | ||
| <AlertCircle className="h-4 w-4 mt-0.5 shrink-0" /> | ||
| <div> | ||
| <p className="font-medium">Import failed — no changes were saved.</p> | ||
| <p className="mt-0.5">{commitError}</p> | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| <div className="flex gap-3"> | ||
| <Button variant="outline" onClick={onReset} disabled={committing}> | ||
| Start over | ||
| </Button> | ||
| <Button onClick={onCommit} disabled={!canCommit || committing}> | ||
| {committing ? ( | ||
| <> | ||
| <Loader2 className="h-4 w-4 mr-2 animate-spin" /> | ||
| Committing… | ||
| </> | ||
| ) : commitError ? ( | ||
| "Retry" | ||
| ) : ( | ||
| "Commit to database" | ||
| )} | ||
| </Button> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { Badge } from "@/components/ui/badge"; | ||
| import { type DiffResult } from "@/services/scheduleImportService"; | ||
|
|
||
| type Props = { diff: DiffResult }; | ||
|
|
||
| export function DiffSummaryBanner({ diff }: Props) { | ||
| const { summary, newArtistNames } = diff; | ||
|
|
||
| const items = [ | ||
| { label: "sets to create", value: summary.setsToCreate, variant: "default" as const }, | ||
| { label: "sets to update", value: summary.setsMatched, variant: "secondary" as const }, | ||
| { label: "new stages", value: summary.newStages, variant: "default" as const }, | ||
| { label: "conflicts", value: summary.setsOrphaned + diff.conflicts.stageNameMismatches.length, variant: "destructive" as const }, | ||
| ].filter((item) => item.value > 0); | ||
|
|
||
| return ( | ||
| <div className="rounded-lg border bg-muted/40 p-4 space-y-3"> | ||
| <div className="flex flex-wrap gap-2"> | ||
| {items.map((item) => ( | ||
| <Badge key={item.label} variant={item.variant}> | ||
| {item.value} {item.label} | ||
| </Badge> | ||
| ))} | ||
| {items.length === 0 && ( | ||
| <span className="text-sm text-muted-foreground">No changes detected.</span> | ||
| )} | ||
| </div> | ||
|
|
||
| {summary.newArtists > 0 && ( | ||
| <p className="text-sm text-muted-foreground"> | ||
| <span className="font-medium text-foreground">{summary.newArtists} new artist{summary.newArtists !== 1 ? "s" : ""}</span> | ||
| {" "}will be created:{" "} | ||
| {newArtistNames.slice(0, 5).join(", ")} | ||
| {newArtistNames.length > 5 && ` and ${newArtistNames.length - 5} more`}. | ||
| </p> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.