From 43a12d08dab2d986760e897f395be3d39e157dac Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 08:30:09 +0200 Subject: [PATCH 1/9] feat(PageHeader): Add back button --- src/shared/components/page-header.tsx | 78 +++++++++++++++++---------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/src/shared/components/page-header.tsx b/src/shared/components/page-header.tsx index e371564..bdb6ded 100644 --- a/src/shared/components/page-header.tsx +++ b/src/shared/components/page-header.tsx @@ -1,48 +1,68 @@ -import { Grid, Typography } from 'antd'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { useBack } from '@refinedev/core'; +import { Button, Grid, Typography } from 'antd'; import type { FC } from 'react'; interface PageHeaderProps { title: string; subtitle?: string; actions?: React.ReactNode; + back?: string; } const { useBreakpoint } = Grid; -const PageHeader: FC = ({ title, subtitle, actions }) => { +const PageHeader: FC = ({ + title, + subtitle, + actions, + back, +}) => { const screens = useBreakpoint(); + const backFn = useBack(); return ( -
-
- + {back ? ( + + ) : null} +
+
+ + {title} + + {subtitle ? ( + + {subtitle} + + ) : null} +
+ {actions ? ( +
{actions}
) : null}
- {actions ? ( -
{actions}
- ) : null}
); }; From e551383e2567e77a471c2cc08b2307fdecff3fa8 Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 11:06:28 +0200 Subject: [PATCH 2/9] refactor: Make more actions generic for boat resource --- public/locales/en.json | 15 ++++++++--- public/locales/fr.json | 15 ++++++++--- src/equipments/pages/list.tsx | 7 ++++-- .../components/resource-actions-menu.tsx} | 25 +++++++++++-------- 4 files changed, 42 insertions(+), 20 deletions(-) rename src/{equipments/components/equipment-actions-menu.tsx => shared/components/resource-actions-menu.tsx} (72%) diff --git a/public/locales/en.json b/public/locales/en.json index ad131ad..bd04867 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -165,6 +165,7 @@ } }, "equipments": { + "single": "equipment", "list": { "title": "Equipment List", "subtitle": "Manage your boat's equipment", @@ -181,10 +182,6 @@ "title": "Equipment Details", "edit": "Edit Equipment" }, - "delete": { - "confirmTitle": "Confirm delete", - "confirmContent": "Do you really want to delete this equipment?" - }, "form": { "labels": { "name": "Name", @@ -264,5 +261,15 @@ "action": "Add people" } } + }, + "shared": { + "actions_menu": { + "delete": { + "confirm": { + "title": "Confirm delete", + "content": "Do you really want to delete this {{resource}}?" + } + } + } } } diff --git a/public/locales/fr.json b/public/locales/fr.json index 9117f3d..d872461 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -165,6 +165,7 @@ } }, "equipments": { + "single": "équipement", "list": { "title": "Liste des équipements", "subtitle": "Gérez les équipements de votre bateau", @@ -181,10 +182,6 @@ "title": "Détails de l'équipement", "edit": "Modifier l'équipement" }, - "delete": { - "confirmTitle": "Confirmer la suppression", - "confirmContent": "Voulez-vous vraiment supprimer cet équipement ?" - }, "form": { "labels": { "name": "Nom", @@ -264,5 +261,15 @@ "action": "Ajouter des personnes" } } + }, + "shared": { + "actions_menu": { + "delete": { + "confirm": { + "title": "Confirmer la suppression", + "content": "Voulez-vous vraiment supprimer cet {{resource}} ?" + } + } + } } } diff --git a/src/equipments/pages/list.tsx b/src/equipments/pages/list.tsx index 7414fc5..a59289a 100644 --- a/src/equipments/pages/list.tsx +++ b/src/equipments/pages/list.tsx @@ -3,9 +3,9 @@ import { Button, Card, Empty, List } from 'antd'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; import { boatSystemList } from '@/boats/utils/boat-system'; -import { EquipmentActionsMenu } from '@/equipments/components/equipment-actions-menu'; import { PageContent } from '@/shared/components/page-content'; import { PageHeader } from '@/shared/components/page-header'; +import { ResourceActionsMenu } from '@/shared/components/resource-actions-menu'; import { SectionHeader } from '@/shared/components/section-header'; import type { Equipment } from '@/shared/types/models'; @@ -81,7 +81,10 @@ const EquipmentList = () => { } description={getEquipmentSubtitle(equipment)} /> - + )} /> diff --git a/src/equipments/components/equipment-actions-menu.tsx b/src/shared/components/resource-actions-menu.tsx similarity index 72% rename from src/equipments/components/equipment-actions-menu.tsx rename to src/shared/components/resource-actions-menu.tsx index 3e18fe0..bd1c7a5 100644 --- a/src/equipments/components/equipment-actions-menu.tsx +++ b/src/shared/components/resource-actions-menu.tsx @@ -4,14 +4,15 @@ import { Button, Dropdown, type MenuProps, Modal } from 'antd'; import type { FC } from 'react'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; -import type { Equipment } from '@/shared/types/models'; -interface EquipmentActionsMenuProps { - equipment: Equipment; +interface ResourceActionsMenuProps { + resource: string; + resourceId: string; } -export const EquipmentActionsMenu: FC = ({ - equipment, +const ResourceActionsMenu: FC = ({ + resource, + resourceId, }) => { const translate = useTranslate(); const { data: boat } = useCurrentBoat(); @@ -21,15 +22,17 @@ export const EquipmentActionsMenu: FC = ({ const handleDelete = () => { Modal.confirm({ - title: translate('equipments.delete.confirmTitle'), - content: translate('equipments.delete.confirmContent'), + title: translate('shared.actions_menu.delete.confirm.title'), + content: translate('shared.actions_menu.delete.confirm.content', { + resource: translate(`${resource}.single`), + }), okText: translate('common.delete'), cancelText: translate('common.cancel'), okType: 'danger', onOk: () => { deleteEquipment({ - resource: 'equipments', - id: equipment.id, + resource, + id: resourceId, }); }, }); @@ -38,7 +41,7 @@ export const EquipmentActionsMenu: FC = ({ const handleMenuClick: MenuProps['onClick'] = ({ key }) => { if (key === 'edit') { go({ - to: `/boats/${boat?.data?.id}/equipments/${equipment.id}/edit`, + to: `/boats/${boat?.data?.id}/${resource}/${resourceId}/edit`, }); } else if (key === 'delete') { handleDelete(); @@ -70,3 +73,5 @@ export const EquipmentActionsMenu: FC = ({ ); }; + +export { ResourceActionsMenu }; From b03d96c22a1100afa3157fd02657f6baf9ec30d9 Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 08:34:46 +0200 Subject: [PATCH 3/9] refactor: Make attachment generic for boat resources --- public/locales/en.json | 10 ++-- public/locales/fr.json | 10 ++-- src/equipments/pages/show.tsx | 4 +- .../components/attachment-list.tsx} | 57 ++++++++----------- 4 files changed, 38 insertions(+), 43 deletions(-) rename src/{equipments/components/equipment-attachment-list.tsx => shared/components/attachment-list.tsx} (74%) diff --git a/public/locales/en.json b/public/locales/en.json index bd04867..1af12f1 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -198,10 +198,6 @@ "name_required": "Equipment name is required.", "system_required": "System is required." } - }, - "attachments": { - "title": "Attachments", - "add": "Add an attachment" } }, "interventions": { @@ -263,6 +259,12 @@ } }, "shared": { + "attachments": { + "title": "Attachments", + "add": "Add an attachment", + "download": "Download", + "delete": "Delete" + }, "actions_menu": { "delete": { "confirm": { diff --git a/public/locales/fr.json b/public/locales/fr.json index d872461..19d5b36 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -198,10 +198,6 @@ "name_required": "Le nom de l'équipement est requis.", "system_required": "Le système est requis." } - }, - "attachments": { - "title": "Pièces jointes", - "add": "Ajouter une pièce jointe" } }, "interventions": { @@ -263,6 +259,12 @@ } }, "shared": { + "attachments": { + "title": "Pièces jointes", + "add": "Ajouter une pièce jointe", + "download": "Télécharger", + "delete": "Supprimer" + }, "actions_menu": { "delete": { "confirm": { diff --git a/src/equipments/pages/show.tsx b/src/equipments/pages/show.tsx index 93f6f7b..c455ebf 100644 --- a/src/equipments/pages/show.tsx +++ b/src/equipments/pages/show.tsx @@ -4,7 +4,7 @@ import dayjs from 'dayjs'; import { useParams } from 'react-router'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; -import { EquipmentAttachmentList } from '@/equipments/components/equipment-attachment-list'; +import { AttachmentList } from '@/shared/components/attachment-list'; import { PageHeader } from '@/shared/components/page-header'; const ShowEquipment = () => { @@ -88,7 +88,7 @@ const ShowEquipment = () => { ) : null} - + ); }; diff --git a/src/equipments/components/equipment-attachment-list.tsx b/src/shared/components/attachment-list.tsx similarity index 74% rename from src/equipments/components/equipment-attachment-list.tsx rename to src/shared/components/attachment-list.tsx index 155e85e..85655eb 100644 --- a/src/equipments/components/equipment-attachment-list.tsx +++ b/src/shared/components/attachment-list.tsx @@ -4,39 +4,34 @@ import { PaperClipOutlined, } from '@ant-design/icons'; import { useCreate, useDelete, useList, useTranslate } from '@refinedev/core'; -import { - Button, - Card, - Col, - List, - Row, - Typography, - Upload, - type UploadProps, -} from 'antd'; +import { Button, Card, Col, 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'; -type EquipmentAttachmentListProps = { - equipmentId?: string; +export type AttachmentListProps = { + resource: string; + resourceId?: string; }; -const EquipmentAttachmentList: FC = ({ - equipmentId, -}) => { +const AttachmentList: FC = ({ resource, resourceId }) => { const translate = useTranslate(); const { data: boat } = useCurrentBoat(); + const attachmentResource = `${resource}_attachments`; + const boatAttachmentBucket = 'boat_attachments'; + const resourceForeignKey = `${resource}_id`; + const { data: attachments } = useList({ - resource: 'equipment_attachments', - filters: [{ field: 'equipment_id', operator: 'eq', value: equipmentId }], + resource: attachmentResource, + filters: [{ field: resourceForeignKey, operator: 'eq', value: resourceId }], }); const { mutate: createAttachment } = useCreate({ - resource: 'equipment_attachments', + resource: attachmentResource, }); const { mutate: deleteAttachment } = useDelete(); @@ -48,10 +43,10 @@ const EquipmentAttachmentList: FC = ({ }) => { try { const uploadedFile = file as File; - const filePath = `${boat?.data?.id}/equipments/${equipmentId}/attachments/${Date.now()}_${uploadedFile.name}`; + const filePath = `${boat?.data?.id}/${resource}s/${resourceId}/attachments/${Date.now()}_${uploadedFile.name}`; const { error: uploadError } = await supabase.storage - .from('boat_attachments') + .from(boatAttachmentBucket) .upload(filePath, file, { upsert: false, contentType: uploadedFile.type, @@ -63,7 +58,7 @@ const EquipmentAttachmentList: FC = ({ createAttachment({ values: { - equipment_id: equipmentId, + [resourceForeignKey]: resourceId, file_name: uploadedFile.name, file_path: filePath, file_type: uploadedFile.type, @@ -78,7 +73,7 @@ const EquipmentAttachmentList: FC = ({ const onDownload = async (attachment: EquipmentAttachment) => { const { data, error } = await supabase.storage - .from('boat_attachments') + .from(boatAttachmentBucket) .createSignedUrl(attachment.file_path, 3600); if (error) { @@ -87,7 +82,6 @@ const EquipmentAttachmentList: FC = ({ } if (data?.signedUrl) { - // Create a hidden link and trigger download in a new tab const link = document.createElement('a'); link.href = data.signedUrl; link.target = '_blank'; @@ -101,11 +95,11 @@ const EquipmentAttachmentList: FC = ({ const onDelete = async (attachment: EquipmentAttachment) => { try { await supabase.storage - .from('boat_attachments') + .from(boatAttachmentBucket) .remove([attachment.file_path]); await deleteAttachment({ - resource: 'equipment_attachments', + resource: attachmentResource, id: attachment.id, }); } catch (error) { @@ -115,9 +109,7 @@ const EquipmentAttachmentList: FC = ({ return (
- - {translate('equipments.attachments.title')} - + = ({
); }; -export { EquipmentAttachmentList }; +export { AttachmentList }; From 92367d20d3dc573c97131ee50648205652e697e7 Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 08:33:29 +0200 Subject: [PATCH 4/9] feat(equipments): Add back button in detail --- public/locales/en.json | 3 ++- public/locales/fr.json | 3 ++- src/equipments/pages/show.tsx | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index 1af12f1..c98a35e 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -180,7 +180,8 @@ }, "show": { "title": "Equipment Details", - "edit": "Edit Equipment" + "edit": "Edit Equipment", + "back": "Back to equipments" }, "form": { "labels": { diff --git a/public/locales/fr.json b/public/locales/fr.json index 19d5b36..6409ab2 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -180,7 +180,8 @@ }, "show": { "title": "Détails de l'équipement", - "edit": "Modifier l'équipement" + "edit": "Modifier l'équipement", + "back": "Retour aux équipements" }, "form": { "labels": { diff --git a/src/equipments/pages/show.tsx b/src/equipments/pages/show.tsx index c455ebf..81b1514 100644 --- a/src/equipments/pages/show.tsx +++ b/src/equipments/pages/show.tsx @@ -26,6 +26,7 @@ const ShowEquipment = () => { } + back={translate('equipments.show.back')} /> From 8bf6005d2038097f199b06c2e005ae95edf5c367 Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 08:16:30 +0200 Subject: [PATCH 5/9] feat(interventions): Improve detail page --- public/locales/en.json | 4 +++- public/locales/fr.json | 4 +++- src/interventions/pages/show.tsx | 21 +++++++++++++++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index c98a35e..8ed7109 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -210,7 +210,9 @@ "title": "Add an intervention" }, "show": { - "edit": "Edit Intervention" + "title": "Intervention Details", + "edit": "Edit Intervention", + "back": "Back to interventions" }, "edit": { "title": "Edit Intervention" diff --git a/public/locales/fr.json b/public/locales/fr.json index 6409ab2..79c34b7 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -210,7 +210,9 @@ "title": "Ajouter une intervention" }, "show": { - "edit": "Modifier l'intervention" + "title": "Détails de l'intervention", + "edit": "Modifier l'intervention", + "back": "Retour aux interventions" }, "edit": { "title": "Editer l'intervention" diff --git a/src/interventions/pages/show.tsx b/src/interventions/pages/show.tsx index 9ecf4e7..0a12077 100644 --- a/src/interventions/pages/show.tsx +++ b/src/interventions/pages/show.tsx @@ -1,9 +1,8 @@ import { Link, useOne, useTranslate } from '@refinedev/core'; -import { Button, Typography } from 'antd'; +import { Button, Card, Typography } from 'antd'; import { useParams } from 'react-router'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; -import { PageContent } from '@/shared/components/page-content'; import { PageHeader } from '@/shared/components/page-header'; const ShowIntervention = () => { @@ -19,7 +18,7 @@ const ShowIntervention = () => { return ( <> { } + back={translate('interventions.show.back')} /> - + - {intervention?.data.description} + {translate('interventions.form.labels.title')}: + {intervention?.data.title} - + {intervention?.data.description ? ( + + + {translate('interventions.form.labels.description')}: + + {intervention.data.description} + + ) : null} + ); }; From e8197067078f51b91455b26480e10eaa244033da Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 09:41:13 +0200 Subject: [PATCH 6/9] feat(interventions): Add costs fields --- public/locales/en.json | 17 +++- public/locales/fr.json | 17 +++- .../components/intervention-form.tsx | 94 ++++++++++++++++++- src/interventions/pages/show.tsx | 17 ++++ src/interventions/utils/cost.ts | 31 ++++++ ...1064112_add_cost_to_intervention_table.sql | 7 ++ supabase/schemas/interventions.sql | 3 + 7 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 src/interventions/utils/cost.ts create mode 100644 supabase/migrations/20250731064112_add_cost_to_intervention_table.sql diff --git a/public/locales/en.json b/public/locales/en.json index 8ed7109..e9916ab 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -212,7 +212,10 @@ "show": { "title": "Intervention Details", "edit": "Edit Intervention", - "back": "Back to interventions" + "back": "Back to interventions", + "total_cost": "Total cost", + "labor_cost": "Labor", + "supply_cost": "Supplies" }, "edit": { "title": "Edit Intervention" @@ -221,7 +224,17 @@ "labels": { "title": "Title", "date": "Date", - "description": "Description" + "description": "Description", + "cost_group": { + "title": "Costs", + "labor": "Labor", + "supply": "Supplies", + "total": "Total" + } + }, + "actions": { + "breakdown_costs": "Break down costs", + "simplify_costs": "Simplify costs" } } }, diff --git a/public/locales/fr.json b/public/locales/fr.json index 79c34b7..c3e8596 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -212,7 +212,10 @@ "show": { "title": "Détails de l'intervention", "edit": "Modifier l'intervention", - "back": "Retour aux interventions" + "back": "Retour aux interventions", + "total_cost": "Coût total", + "labor_cost": "Main d'œuvre", + "supply_cost": "Fournitures" }, "edit": { "title": "Editer l'intervention" @@ -221,7 +224,17 @@ "labels": { "title": "Titre", "date": "Date", - "description": "Description" + "description": "Description", + "cost_group": { + "title": "Coûts", + "labor": "Main d'œuvre", + "supply": "Fournitures", + "total": "Total" + } + }, + "actions": { + "breakdown_costs": "Détailler les coûts", + "simplify_costs": "Simplifier les coûts" } } }, diff --git a/src/interventions/components/intervention-form.tsx b/src/interventions/components/intervention-form.tsx index 5fea87f..c3b1f89 100644 --- a/src/interventions/components/intervention-form.tsx +++ b/src/interventions/components/intervention-form.tsx @@ -1,7 +1,9 @@ +import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons'; import { useTranslate } from '@refinedev/core'; -import { DatePicker, Form, Input } from 'antd'; +import { Button, DatePicker, Form, Input, Space } from 'antd'; import dayjs from 'dayjs'; import type { FC } from 'react'; +import { useState } from 'react'; interface InterventionFormProps { formProps: any; @@ -13,6 +15,7 @@ const InterventionForm: FC = ({ handleOnFinish, }) => { const translate = useTranslate(); + const [isDetailedCost, setDetailedCost] = useState(false); return (
@@ -35,6 +38,95 @@ const InterventionForm: FC = ({ > + +
+ + + + +
+
); }; diff --git a/src/interventions/pages/show.tsx b/src/interventions/pages/show.tsx index 0a12077..38ad057 100644 --- a/src/interventions/pages/show.tsx +++ b/src/interventions/pages/show.tsx @@ -4,6 +4,7 @@ import { useParams } from 'react-router'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; import { PageHeader } from '@/shared/components/page-header'; +import { getCostCalculationString } from '../utils/cost'; const ShowIntervention = () => { const { data: boat } = useCurrentBoat(); @@ -43,6 +44,22 @@ const ShowIntervention = () => { {intervention.data.description} ) : null} + {intervention?.data.total_cost ? ( + <> + + {translate('interventions.show.total_cost')}: + {intervention.data.total_cost} € + + + {getCostCalculationString({ + supplyCost: intervention.data.supply_cost, + laborCost: intervention.data.labor_cost, + totalCost: intervention.data.total_cost, + translate, + })} + + + ) : null} ); diff --git a/src/interventions/utils/cost.ts b/src/interventions/utils/cost.ts new file mode 100644 index 0000000..b348595 --- /dev/null +++ b/src/interventions/utils/cost.ts @@ -0,0 +1,31 @@ +export function getCostCalculationString({ + supplyCost, + laborCost, + totalCost, + translate, +}: { + translate: (key: string) => string; + supplyCost?: number; + laborCost?: number; + totalCost?: number; +}): string { + if (supplyCost === undefined && laborCost === undefined) return ''; + const parts: string[] = []; + if (supplyCost !== undefined) { + parts.push( + `${supplyCost} € (${translate('interventions.show.supply_cost')})`, + ); + } + if (supplyCost !== undefined && laborCost !== undefined) { + parts.push(' + '); + } + if (laborCost !== undefined) { + parts.push( + `${laborCost} € (${translate('interventions.show.labor_cost')})`, + ); + } + if (totalCost !== undefined) { + parts.push(` = ${totalCost} €`); + } + return parts.join(''); +} diff --git a/supabase/migrations/20250731064112_add_cost_to_intervention_table.sql b/supabase/migrations/20250731064112_add_cost_to_intervention_table.sql new file mode 100644 index 0000000..1043887 --- /dev/null +++ b/supabase/migrations/20250731064112_add_cost_to_intervention_table.sql @@ -0,0 +1,7 @@ +alter table "public"."interventions" add column "labor_cost" numeric; + +alter table "public"."interventions" add column "supply_cost" numeric; + +alter table "public"."interventions" add column "total_cost" numeric; + + diff --git a/supabase/schemas/interventions.sql b/supabase/schemas/interventions.sql index b7fad5c..cb5b6ff 100644 --- a/supabase/schemas/interventions.sql +++ b/supabase/schemas/interventions.sql @@ -4,6 +4,9 @@ CREATE TABLE public.interventions ( description text, title text NOT NULL, date timestamp with time zone NOT NULL, + total_cost numeric, + labor_cost numeric, + supply_cost numeric, created_at timestamp with time zone NOT NULL DEFAULT now(), constraint interventions_pkey primary key (id), constraint interventions_boat_id_fkey foreign key (boat_id) references public.boats(id) From 8f0671375894baa0c601423b31bdb1ffed5a3678 Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 09:45:58 +0200 Subject: [PATCH 7/9] feat(interventions): Improve add and edit form --- public/locales/en.json | 6 ++++- public/locales/fr.json | 6 ++++- .../components/intervention-form.tsx | 25 ++++++++++++++----- src/interventions/pages/add.tsx | 8 +++++- src/interventions/pages/edit.tsx | 11 ++++++-- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index e9916ab..0657ecf 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -224,7 +224,7 @@ "labels": { "title": "Title", "date": "Date", - "description": "Description", + "description": "Notes", "cost_group": { "title": "Costs", "labor": "Labor", @@ -232,6 +232,10 @@ "total": "Total" } }, + "validation": { + "title_required": "Title is required.", + "date_required": "Date is required." + }, "actions": { "breakdown_costs": "Break down costs", "simplify_costs": "Simplify costs" diff --git a/public/locales/fr.json b/public/locales/fr.json index c3e8596..7f401cf 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -224,7 +224,7 @@ "labels": { "title": "Titre", "date": "Date", - "description": "Description", + "description": "Notes", "cost_group": { "title": "Coûts", "labor": "Main d'œuvre", @@ -232,6 +232,10 @@ "total": "Total" } }, + "validation": { + "title_required": "Le titre est requis.", + "date_required": "La date est requise." + }, "actions": { "breakdown_costs": "Détailler les coûts", "simplify_costs": "Simplifier les coûts" diff --git a/src/interventions/components/intervention-form.tsx b/src/interventions/components/intervention-form.tsx index c3b1f89..b3608af 100644 --- a/src/interventions/components/intervention-form.tsx +++ b/src/interventions/components/intervention-form.tsx @@ -22,12 +22,12 @@ const InterventionForm: FC = ({ - - - @@ -35,9 +35,22 @@ const InterventionForm: FC = ({ label={translate('interventions.form.labels.date')} name="date" getValueProps={(value) => ({ value: value ? dayjs(value) : null })} + rules={[ + { + required: true, + message: translate('interventions.form.validation.date_required'), + }, + ]} > + + + + { const { data: boat } = useCurrentBoat(); const translate = useTranslate(); + const go = useGo(); const { formProps, saveButtonProps, onFinish } = useForm({ resource: 'interventions', action: 'create', + redirect: false, }); const handleOnFinish = (values: InterventionFormValues) => { @@ -22,6 +24,10 @@ const AddIntervention = () => { boat_id: boat?.data.id, ...values, }); + go({ + to: `/boats/${boat?.data.id}/interventions`, + type: 'replace', + }); }; return ( diff --git a/src/interventions/pages/edit.tsx b/src/interventions/pages/edit.tsx index fc22898..f33e220 100644 --- a/src/interventions/pages/edit.tsx +++ b/src/interventions/pages/edit.tsx @@ -1,5 +1,5 @@ import { Edit, useForm } from '@refinedev/antd'; -import { useTranslate } from '@refinedev/core'; +import { useBack, useTranslate } from '@refinedev/core'; import { useParams } from 'react-router'; import { InterventionForm } from '@/interventions/components/intervention-form'; @@ -7,19 +7,26 @@ import type { UpdateIntervention } from '@/shared/types/models'; const EditIntervention = () => { const { interventionId } = useParams<{ interventionId: string }>(); + const back = useBack(); const { formProps, saveButtonProps, onFinish } = useForm({ resource: 'interventions', action: 'edit', id: interventionId, + redirect: false, }); const translate = useTranslate(); + const handleOnFinish = (values: any) => { + onFinish(values); + back(); + }; + return ( - + ); }; From 84764b934562176b5005c640445874afec08c3a7 Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 10:45:44 +0200 Subject: [PATCH 8/9] feat(interventions): Improve list with more button and actions --- public/locales/en.json | 6 +- public/locales/fr.json | 6 +- src/interventions/pages/list.tsx | 95 ++++++++++++++++++++++++-------- src/shared/types/models.ts | 2 + 4 files changed, 83 insertions(+), 26 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index 0657ecf..2d1a191 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -202,9 +202,11 @@ } }, "interventions": { + "single": "intervention", "list": { - "title": "Intervention List", - "add": "Add an intervention" + "title": "Interventions", + "add": "Add intervention", + "subtitle": "Done on {{date}}" }, "add": { "title": "Add an intervention" diff --git a/public/locales/fr.json b/public/locales/fr.json index 7f401cf..5e10621 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -202,9 +202,11 @@ } }, "interventions": { + "single": "intervention", "list": { - "title": "Liste des interventions", - "add": "Ajouter une intervention" + "title": "Interventions", + "add": "Ajouter une intervention", + "subtitle": "Réalisée le {{date}}" }, "add": { "title": "Ajouter une intervention" diff --git a/src/interventions/pages/list.tsx b/src/interventions/pages/list.tsx index 9578969..3b46bb9 100644 --- a/src/interventions/pages/list.tsx +++ b/src/interventions/pages/list.tsx @@ -1,17 +1,23 @@ -import { Link, useList, useTranslate } from '@refinedev/core'; +import { Link, useInfiniteList, useTranslation } from '@refinedev/core'; import { Button, List } from 'antd'; +import { Fragment } from 'react/jsx-runtime'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; import { PageContent } from '@/shared/components/page-content'; import { PageHeader } from '@/shared/components/page-header'; +import { ResourceActionsMenu } from '@/shared/components/resource-actions-menu'; +import type { Intervention } from '@/shared/types/models'; const InterventionList = () => { const { data: boat } = useCurrentBoat(); - const { data: interventions } = useList({ - resource: 'interventions', - }); + const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = + useInfiniteList({ + resource: 'interventions', + pagination: { pageSize: 50 }, + sorters: [{ field: 'id', order: 'desc' }], + }); - const translate = useTranslate(); + const { getLocale, translate } = useTranslation(); return ( <> @@ -26,25 +32,70 @@ const InterventionList = () => { } /> - ( - - - {intervention.title} - - } - description={intervention.description} + {data?.pages?.map(({ data }) => ( + 0 + ? `interventions-page-${data[0].id}` + : 'interventions-page-empty' + } + > + {data.length > 0 ? ( + ( + + + {intervention.title} + + } + description={ + intervention.date + ? translate('interventions.list.subtitle', { + date: new Date( + intervention.date, + ).toLocaleDateString(getLocale(), { + day: 'numeric', + month: 'long', + year: 'numeric', + }), + }) + : null + } + /> + + + )} /> - - )} - /> + ) : ( +

{translate('interventions.list.empty')}

+ )} + + ))}
+ {hasNextPage ? ( +
+ +
+ ) : null} ); }; diff --git a/src/shared/types/models.ts b/src/shared/types/models.ts index 4668429..34d971c 100644 --- a/src/shared/types/models.ts +++ b/src/shared/types/models.ts @@ -12,4 +12,6 @@ export type EquipmentAttachment = Tables<'equipment_attachments'>; export type Access = Tables<'accesses'>; +export type Intervention = Tables<'interventions'>; +export type InsertIntervention = TablesInsert<'interventions'>; export type UpdateIntervention = TablesUpdate<'interventions'>; From fb52b3c72ef8df7b25d9f51647cc854ae44512a4 Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 31 Jul 2025 11:41:40 +0200 Subject: [PATCH 9/9] feat(interventions): Add attachments --- src/interventions/pages/show.tsx | 2 + ..._create_intervention_attachments_table.sql | 106 ++++++++++++++++++ supabase/schemas/equipment_attachments.sql | 4 +- supabase/schemas/intervention_attachments.sql | 38 +++++++ 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 supabase/migrations/20250731093745_create_intervention_attachments_table.sql create mode 100644 supabase/schemas/intervention_attachments.sql diff --git a/src/interventions/pages/show.tsx b/src/interventions/pages/show.tsx index 38ad057..5bd2b1e 100644 --- a/src/interventions/pages/show.tsx +++ b/src/interventions/pages/show.tsx @@ -3,6 +3,7 @@ import { Button, Card, Typography } from 'antd'; import { useParams } from 'react-router'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; +import { AttachmentList } from '@/shared/components/attachment-list'; import { PageHeader } from '@/shared/components/page-header'; import { getCostCalculationString } from '../utils/cost'; @@ -61,6 +62,7 @@ const ShowIntervention = () => { ) : null} + ); }; diff --git a/supabase/migrations/20250731093745_create_intervention_attachments_table.sql b/supabase/migrations/20250731093745_create_intervention_attachments_table.sql new file mode 100644 index 0000000..ec15369 --- /dev/null +++ b/supabase/migrations/20250731093745_create_intervention_attachments_table.sql @@ -0,0 +1,106 @@ +alter table "public"."equipment_attachments" drop constraint "attachments_equipment_id_fkey"; + +alter table "public"."equipment_attachments" drop constraint "attachments_pkey"; + +drop index if exists "public"."attachments_pkey"; + +create table "public"."intervention_attachments" ( + "id" uuid not null default gen_random_uuid(), + "intervention_id" uuid not null, + "file_path" text not null, + "file_name" text not null, + "file_type" text, + "description" text, + "uploaded_at" timestamp with time zone not null default now() +); + + +alter table "public"."intervention_attachments" enable row level security; + +CREATE UNIQUE INDEX equipment_attachments_pkey ON public.equipment_attachments USING btree (id); + +CREATE INDEX idx_intervention_attachments_intervention_id ON public.intervention_attachments USING btree (intervention_id); + +CREATE UNIQUE INDEX intervention_attachments_pkey ON public.intervention_attachments USING btree (id); + +alter table "public"."equipment_attachments" add constraint "equipment_attachments_pkey" PRIMARY KEY using index "equipment_attachments_pkey"; + +alter table "public"."intervention_attachments" add constraint "intervention_attachments_pkey" PRIMARY KEY using index "intervention_attachments_pkey"; + +alter table "public"."equipment_attachments" add constraint "equipment_attachments_equipment_id_fkey" FOREIGN KEY (equipment_id) REFERENCES equipments(id) ON DELETE CASCADE not valid; + +alter table "public"."equipment_attachments" validate constraint "equipment_attachments_equipment_id_fkey"; + +alter table "public"."intervention_attachments" add constraint "intervention_attachments_intervention_id_fkey" FOREIGN KEY (intervention_id) REFERENCES interventions(id) ON DELETE CASCADE not valid; + +alter table "public"."intervention_attachments" validate constraint "intervention_attachments_intervention_id_fkey"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.check_intervention_access(intervention_id uuid) + RETURNS boolean + LANGUAGE sql + SET search_path TO '' +AS $function$ + SELECT EXISTS ( + SELECT 1 + FROM public.interventions e + JOIN public.accesses a ON e.boat_id = a.boat_id + WHERE e.id = intervention_id + AND a.user_id = (SELECT auth.uid()) + ); +$function$ +; + +grant delete on table "public"."intervention_attachments" to "anon"; + +grant insert on table "public"."intervention_attachments" to "anon"; + +grant references on table "public"."intervention_attachments" to "anon"; + +grant select on table "public"."intervention_attachments" to "anon"; + +grant trigger on table "public"."intervention_attachments" to "anon"; + +grant truncate on table "public"."intervention_attachments" to "anon"; + +grant update on table "public"."intervention_attachments" to "anon"; + +grant delete on table "public"."intervention_attachments" to "authenticated"; + +grant insert on table "public"."intervention_attachments" to "authenticated"; + +grant references on table "public"."intervention_attachments" to "authenticated"; + +grant select on table "public"."intervention_attachments" to "authenticated"; + +grant trigger on table "public"."intervention_attachments" to "authenticated"; + +grant truncate on table "public"."intervention_attachments" to "authenticated"; + +grant update on table "public"."intervention_attachments" to "authenticated"; + +grant delete on table "public"."intervention_attachments" to "service_role"; + +grant insert on table "public"."intervention_attachments" to "service_role"; + +grant references on table "public"."intervention_attachments" to "service_role"; + +grant select on table "public"."intervention_attachments" to "service_role"; + +grant trigger on table "public"."intervention_attachments" to "service_role"; + +grant truncate on table "public"."intervention_attachments" to "service_role"; + +grant update on table "public"."intervention_attachments" to "service_role"; + +create policy "Users can manage attachments of interventions to which they hav" +on "public"."intervention_attachments" +as permissive +for all +to authenticated +using (check_intervention_access(intervention_id)) +with check (check_intervention_access(intervention_id)); + + + diff --git a/supabase/schemas/equipment_attachments.sql b/supabase/schemas/equipment_attachments.sql index 2cd22db..2df4baf 100644 --- a/supabase/schemas/equipment_attachments.sql +++ b/supabase/schemas/equipment_attachments.sql @@ -6,8 +6,8 @@ create table public.equipment_attachments ( file_type text, description text, uploaded_at timestamp with time zone not null default now(), - constraint attachments_pkey primary key (id), - constraint attachments_equipment_id_fkey foreign key (equipment_id) references public.equipments(id) on delete cascade + constraint equipment_attachments_pkey primary key (id), + constraint equipment_attachments_equipment_id_fkey foreign key (equipment_id) references public.equipments(id) on delete cascade ); alter table public.equipment_attachments enable row level security; diff --git a/supabase/schemas/intervention_attachments.sql b/supabase/schemas/intervention_attachments.sql new file mode 100644 index 0000000..733e66d --- /dev/null +++ b/supabase/schemas/intervention_attachments.sql @@ -0,0 +1,38 @@ +create table public.intervention_attachments ( + id uuid not null default gen_random_uuid(), + intervention_id uuid not null, + file_path text not null, + file_name text not null, + file_type text, + description text, + uploaded_at timestamp with time zone not null default now(), + constraint intervention_attachments_pkey primary key (id), + constraint intervention_attachments_intervention_id_fkey foreign key (intervention_id) references public.interventions(id) on delete cascade +); + +alter table public.intervention_attachments enable row level security; + +CREATE OR REPLACE FUNCTION check_intervention_access(intervention_id uuid) +returns boolean +language sql +set search_path = '' +as $$ + SELECT EXISTS ( + SELECT 1 + FROM public.interventions e + JOIN public.accesses a ON e.boat_id = a.boat_id + WHERE e.id = intervention_id + AND a.user_id = (SELECT auth.uid()) + ); +$$; + +create policy "Users can manage attachments of interventions to which they have access" +on public.intervention_attachments +to authenticated +using ( + check_intervention_access(intervention_id) +) with check ( + check_intervention_access(intervention_id) +); + +create index if not exists idx_intervention_attachments_intervention_id on public.intervention_attachments(intervention_id);