diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 0f311ab..8e7917e 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -23,6 +23,14 @@ This document guides AI agents to contribute effectively to the VesselVigil proj
- **I18n**: translation files in `public/locales/`.
- **Authentication**: handled via Supabase and Refine, see `src/auth/providers/auth-provider.ts`.
+## Commit messages
+- Use the conventional commits format: `type(scope): Description`.
+- Types include: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`.
+- Use the imperative mood in the description (e.g., `feat: Add new boat management feature`).
+- Include a short description of the change and, if necessary, a longer explanation in the body.
+- Example: `feat(boats): Add boat management page with list and add functionality`.
+- Always write in English
+
## React librairies
- **Refine**: used for data management and UI components.
- **Ant Design**: UI components library.
diff --git a/public/locales/en.json b/public/locales/en.json
index 2d1a191..cf66186 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -282,8 +282,16 @@
},
"shared": {
"attachments": {
- "title": "Attachments",
- "add": "Add an attachment",
+ "photo": {
+ "title": "Photos",
+ "empty": "No photos found",
+ "add": "Add photo"
+ },
+ "document": {
+ "title": "Documents",
+ "empty": "No documents found",
+ "add": "Add document"
+ },
"download": "Download",
"delete": "Delete"
},
diff --git a/public/locales/fr.json b/public/locales/fr.json
index 5e10621..1ffe8a9 100644
--- a/public/locales/fr.json
+++ b/public/locales/fr.json
@@ -282,8 +282,16 @@
},
"shared": {
"attachments": {
- "title": "Pièces jointes",
- "add": "Ajouter une pièce jointe",
+ "photo": {
+ "title": "Photos",
+ "empty": "Aucune photo trouvée",
+ "add": "Ajouter une photo"
+ },
+ "document": {
+ "title": "Documents",
+ "empty": "Aucun document trouvé",
+ "add": "Ajouter un document"
+ },
"download": "Télécharger",
"delete": "Supprimer"
},
diff --git a/src/equipments/pages/show.tsx b/src/equipments/pages/show.tsx
index 81b1514..3eda91f 100644
--- a/src/equipments/pages/show.tsx
+++ b/src/equipments/pages/show.tsx
@@ -89,7 +89,16 @@ const ShowEquipment = () => {
) : null}
-
+
+
>
);
};
diff --git a/src/interventions/pages/list.tsx b/src/interventions/pages/list.tsx
index 3b46bb9..119e550 100644
--- a/src/interventions/pages/list.tsx
+++ b/src/interventions/pages/list.tsx
@@ -14,7 +14,7 @@ const InterventionList = () => {
useInfiniteList({
resource: 'interventions',
pagination: { pageSize: 50 },
- sorters: [{ field: 'id', order: 'desc' }],
+ sorters: [{ field: 'date', order: 'desc' }],
});
const { getLocale, translate } = useTranslation();
diff --git a/src/interventions/pages/show.tsx b/src/interventions/pages/show.tsx
index 5bd2b1e..16b45b1 100644
--- a/src/interventions/pages/show.tsx
+++ b/src/interventions/pages/show.tsx
@@ -62,7 +62,16 @@ const ShowIntervention = () => {
>
) : null}
-
+
+
>
);
};
diff --git a/src/shared/components/attachment-list.tsx b/src/shared/components/attachment-list.tsx
index 85655eb..3b61663 100644
--- a/src/shared/components/attachment-list.tsx
+++ b/src/shared/components/attachment-list.tsx
@@ -4,20 +4,35 @@ import {
PaperClipOutlined,
} from '@ant-design/icons';
import { useCreate, useDelete, useList, useTranslate } from '@refinedev/core';
-import { Button, Card, Col, List, Row, Upload, type UploadProps } from 'antd';
+import {
+ Button,
+ Card,
+ Col,
+ Empty,
+ List,
+ Row,
+ Upload,
+ type UploadProps,
+} from 'antd';
import type { FC } from 'react';
import { useCurrentBoat } from '@/boats/hooks/use-current-boat';
import { supabaseClient as supabase } from '@/core/utils/supabaseClient';
import { SectionHeader } from '@/shared/components/section-header';
import type { EquipmentAttachment } from '@/shared/types/models';
+import { sanitizeFileName } from '@/shared/utils/sanitize-file-name';
export type AttachmentListProps = {
- resource: string;
+ resource: 'intervention' | 'equipment';
resourceId?: string;
+ type: 'photo' | 'document';
};
-const AttachmentList: FC = ({ resource, resourceId }) => {
+const AttachmentList: FC = ({
+ resource,
+ resourceId,
+ type,
+}) => {
const translate = useTranslate();
const { data: boat } = useCurrentBoat();
@@ -27,7 +42,10 @@ const AttachmentList: FC = ({ resource, resourceId }) => {
const { data: attachments } = useList({
resource: attachmentResource,
- filters: [{ field: resourceForeignKey, operator: 'eq', value: resourceId }],
+ filters: [
+ { field: resourceForeignKey, operator: 'eq', value: resourceId },
+ { field: 'type', operator: 'eq', value: type },
+ ],
});
const { mutate: createAttachment } = useCreate({
@@ -43,7 +61,8 @@ const AttachmentList: FC = ({ resource, resourceId }) => {
}) => {
try {
const uploadedFile = file as File;
- const filePath = `${boat?.data?.id}/${resource}s/${resourceId}/attachments/${Date.now()}_${uploadedFile.name}`;
+ const safeName = sanitizeFileName(uploadedFile.name);
+ const filePath = `${boat?.data?.id}/${resource}s/${resourceId}/attachments/${Date.now()}_${safeName}`;
const { error: uploadError } = await supabase.storage
.from(boatAttachmentBucket)
@@ -62,6 +81,7 @@ const AttachmentList: FC = ({ resource, resourceId }) => {
file_name: uploadedFile.name,
file_path: filePath,
file_type: uploadedFile.type,
+ type,
},
});
onSuccess?.('File uploaded successfully!');
@@ -107,9 +127,31 @@ const AttachmentList: FC = ({ resource, resourceId }) => {
}
};
+ if (attachments?.data.length === 0) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
return (
-
+
= ({ resource, resourceId }) => {
)}
/>
}>
- {translate('shared.attachments.add')}
+ {translate(`shared.attachments.${type}.add`)}
diff --git a/src/shared/utils/sanitize-file-name.ts b/src/shared/utils/sanitize-file-name.ts
new file mode 100644
index 0000000..c1bafb8
--- /dev/null
+++ b/src/shared/utils/sanitize-file-name.ts
@@ -0,0 +1,11 @@
+// Cleans a file name for S3/Supabase storage:
+// - removes accents
+// - replaces special characters with _
+// - keeps only letters, numbers, . _ -
+
+export function sanitizeFileName(name: string): string {
+ return name
+ .normalize('NFD') // removes accents
+ .replace(/[\u0300-\u036f]/g, '')
+ .replace(/[^a-zA-Z0-9._-]/g, '_'); // replaces everything except letters, numbers, . _ -
+}
diff --git a/supabase/migrations/20250801073345_add_type_to_attachment_tables.sql b/supabase/migrations/20250801073345_add_type_to_attachment_tables.sql
new file mode 100644
index 0000000..b8bc183
--- /dev/null
+++ b/supabase/migrations/20250801073345_add_type_to_attachment_tables.sql
@@ -0,0 +1,13 @@
+alter table "public"."equipment_attachments" add column "type" text not null default 'document';
+
+alter table "public"."intervention_attachments" add column "type" text not null default 'document';
+
+alter table "public"."equipment_attachments" add constraint "equipment_attachments_type_check" CHECK ((type = ANY (ARRAY['photo'::text, 'document'::text]))) not valid;
+
+alter table "public"."equipment_attachments" validate constraint "equipment_attachments_type_check";
+
+alter table "public"."intervention_attachments" add constraint "intervention_attachments_type_check" CHECK ((type = ANY (ARRAY['photo'::text, 'document'::text]))) not valid;
+
+alter table "public"."intervention_attachments" validate constraint "intervention_attachments_type_check";
+
+
diff --git a/supabase/schemas/equipment_attachments.sql b/supabase/schemas/equipment_attachments.sql
index 2df4baf..fbf2586 100644
--- a/supabase/schemas/equipment_attachments.sql
+++ b/supabase/schemas/equipment_attachments.sql
@@ -4,6 +4,7 @@ create table public.equipment_attachments (
file_path text not null,
file_name text not null,
file_type text,
+ type text not null check (type in ('photo', 'document')) default 'document',
description text,
uploaded_at timestamp with time zone not null default now(),
constraint equipment_attachments_pkey primary key (id),
diff --git a/supabase/schemas/intervention_attachments.sql b/supabase/schemas/intervention_attachments.sql
index 733e66d..2a73156 100644
--- a/supabase/schemas/intervention_attachments.sql
+++ b/supabase/schemas/intervention_attachments.sql
@@ -4,6 +4,7 @@ create table public.intervention_attachments (
file_path text not null,
file_name text not null,
file_type text,
+ type text not null check (type in ('photo', 'document')) default 'document',
description text,
uploaded_at timestamp with time zone not null default now(),
constraint intervention_attachments_pkey primary key (id),