diff --git a/public/locales/en.json b/public/locales/en.json index ad131ad..2d1a191 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", @@ -179,11 +180,8 @@ }, "show": { "title": "Equipment Details", - "edit": "Edit Equipment" - }, - "delete": { - "confirmTitle": "Confirm delete", - "confirmContent": "Do you really want to delete this equipment?" + "edit": "Edit Equipment", + "back": "Back to equipments" }, "form": { "labels": { @@ -201,22 +199,25 @@ "name_required": "Equipment name is required.", "system_required": "System is required." } - }, - "attachments": { - "title": "Attachments", - "add": "Add an attachment" } }, "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" }, "show": { - "edit": "Edit Intervention" + "title": "Intervention Details", + "edit": "Edit Intervention", + "back": "Back to interventions", + "total_cost": "Total cost", + "labor_cost": "Labor", + "supply_cost": "Supplies" }, "edit": { "title": "Edit Intervention" @@ -225,7 +226,21 @@ "labels": { "title": "Title", "date": "Date", - "description": "Description" + "description": "Notes", + "cost_group": { + "title": "Costs", + "labor": "Labor", + "supply": "Supplies", + "total": "Total" + } + }, + "validation": { + "title_required": "Title is required.", + "date_required": "Date is required." + }, + "actions": { + "breakdown_costs": "Break down costs", + "simplify_costs": "Simplify costs" } } }, @@ -264,5 +279,21 @@ "action": "Add people" } } + }, + "shared": { + "attachments": { + "title": "Attachments", + "add": "Add an attachment", + "download": "Download", + "delete": "Delete" + }, + "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..5e10621 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", @@ -179,11 +180,8 @@ }, "show": { "title": "Détails de l'équipement", - "edit": "Modifier l'équipement" - }, - "delete": { - "confirmTitle": "Confirmer la suppression", - "confirmContent": "Voulez-vous vraiment supprimer cet équipement ?" + "edit": "Modifier l'équipement", + "back": "Retour aux équipements" }, "form": { "labels": { @@ -201,22 +199,25 @@ "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": { + "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" }, "show": { - "edit": "Modifier l'intervention" + "title": "Détails de l'intervention", + "edit": "Modifier l'intervention", + "back": "Retour aux interventions", + "total_cost": "Coût total", + "labor_cost": "Main d'œuvre", + "supply_cost": "Fournitures" }, "edit": { "title": "Editer l'intervention" @@ -225,7 +226,21 @@ "labels": { "title": "Titre", "date": "Date", - "description": "Description" + "description": "Notes", + "cost_group": { + "title": "Coûts", + "labor": "Main d'œuvre", + "supply": "Fournitures", + "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" } } }, @@ -264,5 +279,21 @@ "action": "Ajouter des personnes" } } + }, + "shared": { + "attachments": { + "title": "Pièces jointes", + "add": "Ajouter une pièce jointe", + "download": "Télécharger", + "delete": "Supprimer" + }, + "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/pages/show.tsx b/src/equipments/pages/show.tsx index 93f6f7b..81b1514 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 = () => { @@ -26,6 +26,7 @@ const ShowEquipment = () => { } + back={translate('equipments.show.back')} /> @@ -88,7 +89,7 @@ const ShowEquipment = () => { ) : null} - + ); }; diff --git a/src/interventions/components/intervention-form.tsx b/src/interventions/components/intervention-form.tsx index 5fea87f..b3608af 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,27 +15,130 @@ const InterventionForm: FC = ({ handleOnFinish, }) => { const translate = useTranslate(); + const [isDetailedCost, setDetailedCost] = useState(false); return (
+ ({ value: value ? dayjs(value) : null })} + rules={[ + { + required: true, + message: translate('interventions.form.validation.date_required'), + }, + ]} + > + + - + + ({ value: value ? dayjs(value) : null })} + label={translate('interventions.form.labels.cost_group.title')} + style={{ marginBottom: 0 }} > - +
+ + + + +
); diff --git a/src/interventions/pages/add.tsx b/src/interventions/pages/add.tsx index 4a38457..67add15 100644 --- a/src/interventions/pages/add.tsx +++ b/src/interventions/pages/add.tsx @@ -1,5 +1,5 @@ import { Create, useForm } from '@refinedev/antd'; -import { useTranslate } from '@refinedev/core'; +import { useGo, useTranslate } from '@refinedev/core'; import { useCurrentBoat } from '@/boats/hooks/use-current-boat'; import { InterventionForm } from '@/interventions/components/intervention-form'; @@ -11,10 +11,12 @@ interface InterventionFormValues { const AddIntervention = () => { 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 ( - + ); }; 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/interventions/pages/show.tsx b/src/interventions/pages/show.tsx index 9ecf4e7..5bd2b1e 100644 --- a/src/interventions/pages/show.tsx +++ b/src/interventions/pages/show.tsx @@ -1,10 +1,11 @@ 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 { AttachmentList } from '@/shared/components/attachment-list'; import { PageHeader } from '@/shared/components/page-header'; +import { getCostCalculationString } from '../utils/cost'; const ShowIntervention = () => { const { data: boat } = useCurrentBoat(); @@ -19,7 +20,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} + {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/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 }; 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}
); }; 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 }; 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'>; 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/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); 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)