Skip to content

Commit 90e4c4f

Browse files
committed
Merge branch 'import-export'
2 parents edda54b + 2c0e9f8 commit 90e4c4f

18 files changed

Lines changed: 896 additions & 277 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@radix-ui/react-context-menu": "^2.2.1",
2323
"@radix-ui/react-dialog": "^1.0.4",
2424
"@radix-ui/react-label": "^2.0.2",
25-
"@radix-ui/react-popover": "^1.1.1",
25+
"@radix-ui/react-popover": "^1.1.14",
2626
"@radix-ui/react-select": "^1.2.2",
2727
"@radix-ui/react-slot": "^1.0.2",
2828
"@tauri-apps/plugin-autostart": "^2.0.0",

src/_common/business/boat.rules.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface Boat {
2020
name: string;
2121
isInMaintenance?: boolean;
2222
type?: BoatTypeEnum;
23+
note?: string;
2324
}
2425

2526
export interface BoatWithoutUndefined {

src/_common/store/clubOverview.store.functions.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ export const updateBoatNameFn = (
3737
return updatedBoats;
3838
};
3939

40+
export const updateBoatNoteFn = (
41+
boats: ClubOverviewState["boats"],
42+
boatId: string,
43+
note: string
44+
) => {
45+
const updatedBoats = boats.map((boat) => {
46+
if (boat.id === boatId) {
47+
return {
48+
...boat,
49+
note,
50+
};
51+
}
52+
return boat;
53+
});
54+
55+
return updatedBoats;
56+
};
57+
4058
export const toggleBoatIsInMaintenanceFn = (
4159
boats: ClubOverviewState["boats"],
4260
boatId: string

src/_common/store/clubOverview.store.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
toggleBoatIsInMaintenanceFn,
55
updateBoatNameFn,
66
updateBoatTypeFn,
7+
updateBoatNoteFn,
78
} from "./clubOverview.store.functions";
89
import { BoatTypeEnum } from "../business/boat.rules";
910
import { generateBoatId } from "../business/boat.rules";
@@ -19,6 +20,7 @@ export interface ClubOverviewState {
1920
name: string;
2021
isInMaintenance?: boolean;
2122
type?: BoatTypeEnum;
23+
note?: string;
2224
archivedAt?: string | undefined;
2325
}[];
2426
routes: {
@@ -56,6 +58,7 @@ export interface ClubOverviewStoreState {
5658
getBoatById: (boatId: string) => ClubOverviewState["boats"][0] | undefined;
5759
updateBoatType: (boatId: string, type: BoatTypeEnum) => void;
5860
updateBoatName: (boatId: string, name: string) => void;
61+
updateBoatNote: (boatId: string, note: string) => void;
5962
toggleBoatIsInMaintenance: (boatId: string) => void;
6063
addBoat: (boat: { name: string; type?: BoatTypeEnum }) => void;
6164
archiveBoat: (boatId: string) => void;
@@ -160,6 +163,7 @@ export const useClubOverviewStore = create<ClubOverviewStoreState>()(
160163
name: boatName,
161164
isInMaintenance: false,
162165
type: boatType || BoatTypeEnum.OTHER,
166+
note: "",
163167
},
164168
],
165169
},
@@ -200,6 +204,15 @@ export const useClubOverviewStore = create<ClubOverviewStoreState>()(
200204
}));
201205
},
202206

207+
updateBoatNote: (boatId: string, note: string) => {
208+
set((state) => ({
209+
clubOverview: {
210+
...state.clubOverview,
211+
boats: updateBoatNoteFn(state.clubOverview.boats, boatId, note),
212+
},
213+
}));
214+
},
215+
203216
toggleBoatIsInMaintenance: (boatId: string) => {
204217
set((state) => ({
205218
clubOverview: {

src/_common/utils/export.ts

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,48 @@ import { writeTextFile, writeFile, BaseDirectory } from "@tauri-apps/plugin-fs";
33

44
export type ExportType = "xlsx" | "ods" | "json" | "csv";
55

6-
export const exportSpreadsheet = (args: {
6+
export const exportData = (args: {
7+
data: Record<string, string>[];
8+
fileName: string;
9+
fileType: ExportType;
10+
}) => {
11+
const { fileType } = args;
12+
13+
if (fileType === "xlsx" || fileType === "ods") {
14+
return exportSpreadsheet({
15+
...args,
16+
fileType,
17+
});
18+
}
19+
20+
if (fileType === "json") {
21+
return exportJson(args);
22+
}
23+
24+
if (fileType === "csv") {
25+
return exportCsv(args);
26+
}
27+
};
28+
29+
const exportSpreadsheet = (args: {
730
data: Record<string, string | Date>[];
831
fileName: string;
932
fileType: "xlsx" | "ods";
1033
}) => {
1134
const wb = XLSX.utils.book_new();
1235

1336
const data = args.data.map((row) => {
14-
return {
15-
...row,
16-
start_date_time: row.start_date_time
17-
? new Date(row.start_date_time)
18-
: null,
19-
estimated_end_date_time: row.estimated_end_date_time
20-
? new Date(row.estimated_end_date_time)
21-
: null,
22-
end_date_time: row.end_date_time ? new Date(row.end_date_time) : null,
23-
};
37+
const parsedRow: Record<string, unknown> = {};
38+
39+
for (const [key, value] of Object.entries(row)) {
40+
if (isDateString(value)) {
41+
parsedRow[key] = new Date(value as string);
42+
} else {
43+
parsedRow[key] = value;
44+
}
45+
}
46+
47+
return parsedRow;
2448
});
2549

2650
const ws = XLSX.utils.json_to_sheet(data, {
@@ -43,20 +67,8 @@ export const exportSpreadsheet = (args: {
4367
});
4468
};
4569

46-
const saveFile = (args: { file: Uint8Array; fileName: string }) => {
47-
return writeFile(args.fileName, args.file, {
48-
baseDir: BaseDirectory.Download,
49-
});
50-
};
51-
52-
const saveTextFile = (args: { content: string; fileName: string }) => {
53-
return writeTextFile(args.fileName, args.content, {
54-
baseDir: BaseDirectory.Download,
55-
});
56-
};
57-
5870
export const exportJson = (args: {
59-
data: Record<string, string>[];
71+
data: Record<string, unknown>[];
6072
fileName: string;
6173
fileType: ExportType;
6274
}) => {
@@ -68,7 +80,7 @@ export const exportJson = (args: {
6880
});
6981
};
7082

71-
export const exportCsv = (args: {
83+
const exportCsv = (args: {
7284
data: Record<string, string>[];
7385
fileName: string;
7486
fileType: ExportType;
@@ -85,25 +97,22 @@ export const exportCsv = (args: {
8597
});
8698
};
8799

88-
export const exportData = (args: {
89-
data: Record<string, string>[];
90-
fileName: string;
91-
fileType: ExportType;
92-
}) => {
93-
const { fileType } = args;
100+
const saveFile = (args: { file: Uint8Array; fileName: string }) => {
101+
return writeFile(args.fileName, args.file, {
102+
baseDir: BaseDirectory.Download,
103+
});
104+
};
94105

95-
if (fileType === "xlsx" || fileType === "ods") {
96-
return exportSpreadsheet({
97-
...args,
98-
fileType,
99-
});
100-
}
106+
const saveTextFile = (args: { content: string; fileName: string }) => {
107+
return writeTextFile(args.fileName, args.content, {
108+
baseDir: BaseDirectory.Download,
109+
});
110+
};
101111

102-
if (fileType === "json") {
103-
return exportJson(args);
104-
}
112+
const isDateString = (value: unknown): boolean => {
113+
if (typeof value !== "string") return false;
105114

106-
if (fileType === "csv") {
107-
return exportCsv(args);
108-
}
115+
// Check if it's a valid date string (ISO format or other parseable formats)
116+
const date = new Date(value);
117+
return !isNaN(date.getTime()) && value.match(/\d{4}-\d{2}-\d{2}/) !== null;
109118
};

src/_common/utils/importExport.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { open } from "@tauri-apps/plugin-dialog";
2+
import { readTextFile } from "@tauri-apps/plugin-fs";
3+
import { z } from "zod";
4+
import { exportJson } from "./export";
5+
import { useClubOverviewStore } from "../store/clubOverview.store";
6+
import { BoatTypeEnum } from "../business/boat.rules";
7+
import { SeriousnessCategoryEnum } from "../business/seriousness.rules";
8+
import { AgeCategoryEnum } from "../business/ageCategory.rules";
9+
10+
const BoatSchema = z.object({
11+
id: z.string(),
12+
name: z.string(),
13+
isInMaintenance: z.boolean().optional(),
14+
type: z.nativeEnum(BoatTypeEnum).optional(),
15+
note: z.string().optional(),
16+
archivedAt: z.string().optional(),
17+
});
18+
19+
const RouteSchema = z.object({
20+
id: z.string(),
21+
name: z.string(),
22+
archivedAt: z.string().optional(),
23+
});
24+
25+
const RowerSchema = z.object({
26+
id: z.string(),
27+
name: z.string(),
28+
archivedAt: z.string().optional(),
29+
type: z.nativeEnum(SeriousnessCategoryEnum).optional(),
30+
category: z.nativeEnum(AgeCategoryEnum).optional(),
31+
});
32+
33+
const ExportDataSchema = z.object({
34+
boats: z.array(BoatSchema),
35+
routes: z.array(RouteSchema),
36+
rowers: z.array(RowerSchema),
37+
});
38+
39+
const ExportDataArraySchema = z.array(ExportDataSchema);
40+
41+
export type ExportData = z.infer<typeof ExportDataSchema>;
42+
43+
export const exportAllData = async () => {
44+
const store = useClubOverviewStore.getState();
45+
const boats = store.clubOverview.boats;
46+
const routes = store.clubOverview.routes;
47+
const rowers = store.clubOverview.rowers;
48+
49+
const exportData: ExportData = {
50+
boats,
51+
routes,
52+
rowers,
53+
};
54+
55+
const fileName = `rowingbeacon-export-${
56+
new Date().toISOString().split("T")[0]
57+
}.json`;
58+
59+
// Validate data before export
60+
const validatedData = ExportDataArraySchema.parse([exportData]);
61+
62+
await exportJson({
63+
data: validatedData,
64+
fileName,
65+
fileType: "json",
66+
});
67+
68+
return fileName;
69+
};
70+
71+
export const importAllData = async () => {
72+
const filePath = await open({
73+
multiple: false,
74+
filters: [
75+
{
76+
name: "JSON",
77+
extensions: ["json"],
78+
},
79+
],
80+
});
81+
82+
if (!filePath) {
83+
return null;
84+
}
85+
86+
const content = await readTextFile(filePath);
87+
const parsedJson: unknown = JSON.parse(content);
88+
89+
// Try to validate as array first, then as single object
90+
let importData: ExportData;
91+
92+
const arrayResult = ExportDataArraySchema.safeParse(parsedJson);
93+
if (arrayResult.success) {
94+
importData = arrayResult.data[0];
95+
} else {
96+
const singleResult = ExportDataSchema.safeParse(parsedJson);
97+
if (singleResult.success) {
98+
importData = singleResult.data;
99+
} else {
100+
throw new Error(
101+
"Le fichier ne contient pas de données valides. Erreur de validation : " +
102+
singleResult.error.message
103+
);
104+
}
105+
}
106+
107+
const store = useClubOverviewStore.getState();
108+
const allBoats = store.clubOverview.boats;
109+
const allRoutes = store.clubOverview.routes;
110+
const allRowers = store.clubOverview.rowers;
111+
112+
let boatsAdded = 0;
113+
let routesAdded = 0;
114+
let rowersAdded = 0;
115+
116+
if (importData.boats) {
117+
for (const boat of importData.boats) {
118+
if (boat.archivedAt) continue;
119+
120+
const existingBoat = allBoats.find(
121+
(b) => b.name.toLowerCase() === boat.name.toLowerCase()
122+
);
123+
if (!existingBoat) {
124+
store.addBoat({
125+
name: boat.name,
126+
type: boat.type,
127+
});
128+
boatsAdded++;
129+
}
130+
}
131+
}
132+
133+
if (importData.routes) {
134+
for (const route of importData.routes) {
135+
if (route.archivedAt) continue;
136+
137+
const existingRoute = allRoutes.find(
138+
(r) => r.name.toLowerCase() === route.name.toLowerCase()
139+
);
140+
if (!existingRoute) {
141+
store.addRoute(route.name);
142+
routesAdded++;
143+
}
144+
}
145+
}
146+
147+
if (importData.rowers) {
148+
for (const rower of importData.rowers) {
149+
if (rower.archivedAt) continue;
150+
151+
const existingRower = allRowers.find(
152+
(r) => r.name.toLowerCase() === rower.name.toLowerCase()
153+
);
154+
if (!existingRower) {
155+
store.addRower(rower.name, rower.category, rower.type);
156+
rowersAdded++;
157+
}
158+
}
159+
}
160+
161+
return {
162+
boats: boatsAdded,
163+
routes: routesAdded,
164+
rowers: rowersAdded,
165+
};
166+
};

0 commit comments

Comments
 (0)